tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74# --- Main constants: 75 76NANO = 0.000000001 # SI-constant nano = 10^-9 77 78 79def NanoToFloat(units: str, nano: int) -> float: 80 """ 81 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 82 83 `NanoToFloat(units="2", nano=500000000) -> 2.5` 84 85 `NanoToFloat(units="0", nano=50000000) -> 0.05` 86 87 :param units: integer string or integer parameter that represents the integer part of number 88 :param nano: integer string or integer parameter that represents the fractional part of number 89 :return: float view of number 90 """ 91 return int(units) + int(nano) * NANO 92 93 94def FloatToNano(number: float) -> dict: 95 """ 96 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 97 98 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 99 100 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 101 102 :param number: float number 103 :return: nano-type view of number: `{"units": "string", "nano": integer}` 104 """ 105 splitByPoint = str(number).split(".") 106 frac = 0 107 108 if len(splitByPoint) > 1: 109 if len(splitByPoint[1]) <= 9: 110 frac = int("{}{}".format( 111 int(splitByPoint[1]), 112 "0" * (9 - len(splitByPoint[1])), 113 )) 114 115 if (number < 0) and (frac > 0): 116 frac = -frac 117 118 return {"units": str(int(number)), "nano": frac} 119 120 121def GetDatesAsString(start: str = None, end: str = None) -> tuple: 122 """ 123 Create tuple of date and time strings with timezone parsed from user-friendly date. 124 125 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 126 127 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 128 An error exception will occur if input date has incorrect format. 129 130 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 131 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 132 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 133 Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago. 134 135 Also, you can use keywords for start if `end=None`: 136 `today` (from 00:00:00 to the end of current day), 137 `yesterday` (-1 day from 00:00:00 to 23:59:59), 138 `week` (-7 day from 00:00:00 to the end of current day), 139 `month` (-30 day from 00:00:00 to the end of current day), 140 `year` (-365 day from 00:00:00 to the end of current day), 141 142 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 143 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 144 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 145 """ 146 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 147 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 148 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 149 150 # time between start and the end of the current day: 151 if start is None or start.lower() == "today": 152 pass 153 154 # from start of the last day to the end of the last day: 155 elif start.lower() == "yesterday": 156 s -= timedelta(days=1) 157 e -= timedelta(days=1) 158 159 # week (-7 day from 00:00:00 to the end of the current day): 160 elif start.lower() == "week": 161 s -= timedelta(days=6) # +1 current day already taken into account 162 163 # month (-30 day from 00:00:00 to the end of current day): 164 elif start.lower() == "month": 165 s -= timedelta(days=29) # +1 current day already taken into account 166 167 # year (-365 day from 00:00:00 to the end of current day): 168 elif start.lower() == "year": 169 s -= timedelta(days=364) # +1 current day already taken into account 170 171 # -N days ago to the end of current day: 172 elif start.startswith('-') and start[1:].isdigit(): 173 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 174 175 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 176 else: 177 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 178 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 179 180 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 181 s = s.strftime(TKS_DATE_TIME_FORMAT) 182 e = e.strftime(TKS_DATE_TIME_FORMAT) 183 184 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 185 186 return s, e 187 188 189class TinkoffBrokerServer: 190 """ 191 This class implements methods to work with Tinkoff broker server. 192 193 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 194 195 About `token`: https://tinkoff.github.io/investAPI/token/ 196 """ 197 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 198 """ 199 Main class init. 200 201 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 202 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 203 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 204 :param useCache: use default cache file with raw data to use instead of `iList`. 205 True by default. Cache is auto-update if new day has come. 206 If you don't want to use cache and always updates raw data then set `useCache=False`. 207 :param defaultCache: path to default cache file. `dump.json` by default. 208 """ 209 if token is None or not token: 210 try: 211 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 212 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 213 214 except KeyError: 215 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 216 raise Exception("Token required") 217 218 else: 219 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 220 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 221 222 if accountId is None or not accountId: 223 try: 224 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 225 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 226 227 except KeyError: 228 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 229 230 else: 231 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 232 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 233 234 self.version = __version__ # duplicate here used TKSBrokerAPI main version 235 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 236 237 Latest version: https://pypi.org/project/tksbrokerapi/ 238 """ 239 240 self.aliases = TKS_TICKER_ALIASES 241 """Some aliases instead official tickers. 242 243 See also: `TKSEnums.TKS_TICKER_ALIASES` 244 """ 245 246 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 247 248 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 249 250 self.ticker = "" 251 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 252 253 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 254 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 255 256 See also: `SearchByTicker()`, `SearchInstruments()`. 257 """ 258 259 self.figi = "" 260 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 261 262 See also: `SearchByFIGI()`, `SearchInstruments()`. 263 """ 264 265 self.depth = 1 266 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 267 268 See also: `GetCurrentPrices()`. 269 """ 270 271 self.server = r"https://invest-public-api.tinkoff.ru/rest" 272 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 273 274 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 275 """ 276 277 uLogger.debug("Broker API server: {}".format(self.server)) 278 279 self.timeout = 15 280 """Server operations timeout in seconds. Default: `15`. 281 282 See also: `SendAPIRequest()`. 283 """ 284 285 self.headers = { 286 "Content-Type": "application/json", 287 "accept": "application/json", 288 "Authorization": "Bearer {}".format(self.token), 289 "x-app-name": "Tim55667757.TKSBrokerAPI", 290 } 291 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 292 293 See also: `SendAPIRequest()`. 294 """ 295 296 self.body = None 297 """Request body which send to broker server. Default: `None`. 298 299 See also: `SendAPIRequest()`. 300 """ 301 302 self.moreDebug = False 303 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 304 305 self.historyFile = None 306 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 307 308 See also: `History()`. 309 """ 310 311 self.htmlHistoryFile = "index.html" 312 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 313 314 See also: `ShowHistoryChart()`. 315 """ 316 317 self.instrumentsFile = "instruments.md" 318 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 319 320 See also: `ShowInstrumentsInfo()`. 321 """ 322 323 self.searchResultsFile = "search-results.md" 324 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 325 326 See also: `SearchInstruments()`. 327 """ 328 329 self.pricesFile = "prices.md" 330 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 331 332 See also: `GetListOfPrices()`. 333 """ 334 335 self.infoFile = "info.md" 336 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 337 338 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 339 """ 340 341 self.bondsXLSXFile = "ext-bonds.xlsx" 342 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 343 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 344 345 See also: `ExtendBondsData()`. 346 """ 347 348 self.calendarFile = "calendar.md" 349 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 350 351 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 352 353 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 354 """ 355 356 self.overviewFile = "overview.md" 357 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 358 359 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 360 """ 361 362 self.overviewDigestFile = "overview-digest.md" 363 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 364 365 See also: `Overview()` with parameter `details="digest"`. 366 """ 367 368 self.overviewPositionsFile = "overview-positions.md" 369 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 370 371 See also: `Overview()` with parameter `details="positions"`. 372 """ 373 374 self.overviewOrdersFile = "overview-orders.md" 375 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 376 377 See also: `Overview()` with parameter `details="orders"`. 378 """ 379 380 self.overviewAnalyticsFile = "overview-analytics.md" 381 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 382 383 See also: `Overview()` with parameter `details="analytics"`. 384 """ 385 386 self.overviewBondsCalendarFile = "overview-calendar.md" 387 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 388 389 See also: `Overview()` with parameter `details="calendar"`. 390 """ 391 392 self.reportFile = "deals.md" 393 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 394 395 See also: `Deals()`. 396 """ 397 398 self.withdrawalLimitsFile = "limits.md" 399 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 400 401 See also: `OverviewLimits()` and `RequestLimits()`. 402 """ 403 404 self.userInfoFile = "user-info.md" 405 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 406 407 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 408 """ 409 410 self.userAccountsFile = "accounts.md" 411 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 412 413 See also: `OverviewAccounts()`, `RequestAccounts()`. 414 """ 415 416 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 417 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 418 419 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 420 421 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 422 """ 423 424 self.iList = None # init iList for raw instruments data 425 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 426 427 See also: `Listing()`, `DumpInstruments()`. 428 """ 429 430 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 431 if useCache: 432 if os.path.exists(self.iListDumpFile): 433 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 434 curTime = datetime.now(tzutc()) 435 436 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 437 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 438 439 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 440 441 else: 442 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 443 444 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 445 os.path.abspath(self.iListDumpFile), 446 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 447 )) 448 449 else: 450 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 451 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 452 453 else: 454 self.iList = self.Listing() # request new raw instruments data from broker server 455 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 456 457 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 458 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 459 460 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 461 """ 462 463 def _ParseJSON(self, rawData="{}") -> dict: 464 """ 465 Parse JSON from response string. 466 467 :param rawData: this is a string with JSON-formatted text. 468 :return: JSON (dictionary), parsed from server response string. 469 """ 470 responseJSON = json.loads(rawData) if rawData else {} 471 472 if self.moreDebug: 473 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 474 475 return responseJSON 476 477 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 478 """ 479 Send GET or POST request to broker server and receive JSON object. 480 481 self.header: must be defining with dictionary of headers. 482 self.body: if define then used as request body. None by default. 483 self.timeout: global request timeout, 15 seconds by default. 484 :param url: url with REST request. 485 :param reqType: send "GET" or "POST" request. "GET" by default. 486 :param retry: how many times retry after first request if an 5xx server errors occurred. 487 :param pause: sleep time in seconds between retries. 488 :return: response JSON (dictionary) from broker. 489 """ 490 if reqType not in ("GET", "POST"): 491 uLogger.error("You can define request type: 'GET' or 'POST'!") 492 raise Exception("Incorrect value") 493 494 if self.moreDebug: 495 uLogger.debug("Request parameters:") 496 uLogger.debug(" - REST API URL: {}".format(url)) 497 uLogger.debug(" - request type: {}".format(reqType)) 498 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 499 uLogger.debug(" - body:\n{}".format(self.body)) 500 501 # fast hack to avoid all operations with some tickers/FIGI 502 responseJSON = {} 503 oK = True 504 for item in self.exclude: 505 if item in url: 506 if self.moreDebug: 507 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 508 509 oK = False 510 break 511 512 if oK: 513 counter = 0 514 response = None 515 errMsg = "" 516 517 while not response and counter <= retry: 518 if reqType == "GET": 519 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 520 521 if reqType == "POST": 522 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 523 524 if self.moreDebug: 525 uLogger.debug("Response:") 526 uLogger.debug(" - status code: {}".format(response.status_code)) 527 uLogger.debug(" - reason: {}".format(response.reason)) 528 uLogger.debug(" - body length: {}".format(len(response.text))) 529 uLogger.debug(" - headers:\n{}".format(response.headers)) 530 531 # Server returns some headers: 532 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 533 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 534 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 535 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 536 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 537 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 538 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 539 sleep(rateLimitWait) 540 541 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 542 if 400 <= response.status_code < 500: 543 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 544 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 545 counter = retry + 1 546 547 if 500 <= response.status_code < 600: 548 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 549 uLogger.debug(" - not oK, {}".format(errMsg)) 550 counter += 1 551 552 if counter <= retry: 553 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 554 sleep(pause) 555 556 responseJSON = self._ParseJSON(rawData=response.text) 557 558 if errMsg: 559 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 560 uLogger.error(" - not oK, {}".format(errMsg)) 561 562 return responseJSON 563 564 def _IUpdater(self, iType: str) -> tuple: 565 """ 566 Request instrument by type from server. See available API methods for instruments: 567 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 568 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 569 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 570 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 571 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 572 573 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 574 :return: tuple with iType name and list of available instruments of current type for defined user token. 575 """ 576 result = [] 577 578 if iType in TKS_INSTRUMENTS: 579 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 580 581 # all instruments have the same body in API v2 requests: 582 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 583 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 584 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 585 586 return iType, result 587 588 def _IWrapper(self, kwargs): 589 """ 590 Wrapper runs instrument's update method `_IUpdater()`. 591 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 592 """ 593 return self._IUpdater(**kwargs) 594 595 def Listing(self) -> dict: 596 """ 597 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 598 599 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 600 """ 601 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 602 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 603 604 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 605 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 606 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 607 608 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 609 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 610 poolUpdater.close() 611 612 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 613 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 614 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 615 616 # calculate minimum price increment (step) for all instruments and set up instrument's type: 617 for iType in iList.keys(): 618 for ticker in iList[iType]: 619 iList[iType][ticker]["type"] = iType 620 621 if "minPriceIncrement" in iList[iType][ticker].keys(): 622 iList[iType][ticker]["step"] = NanoToFloat( 623 iList[iType][ticker]["minPriceIncrement"]["units"], 624 iList[iType][ticker]["minPriceIncrement"]["nano"], 625 ) 626 627 else: 628 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 629 630 return iList 631 632 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 633 """ 634 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 635 636 See also: `DumpInstruments()`, `Listing()`. 637 638 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 639 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 640 """ 641 if self.iListDumpFile is None or not self.iListDumpFile: 642 uLogger.error("Output name of dump file must be defined!") 643 raise Exception("Filename required") 644 645 if not self.iList or forceUpdate: 646 self.iList = self.Listing() 647 648 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 649 650 # Save as XLSX with separated sheets for every type of instruments: 651 with pd.ExcelWriter( 652 path=xlsxDumpFile, 653 date_format=TKS_DATE_FORMAT, 654 datetime_format=TKS_DATE_TIME_FORMAT, 655 mode="w", 656 ) as writer: 657 for iType in TKS_INSTRUMENTS: 658 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 659 df = df[sorted(df)] # sorted by column names 660 df = df.applymap( 661 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 662 na_action="ignore", 663 ) # converting numbers from nano-type to float in every cell 664 df.to_excel( 665 writer, 666 sheet_name=iType, 667 encoding="UTF-8", 668 freeze_panes=(1, 1), 669 ) # saving as XLSX-file with freeze first row and column as headers 670 671 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 672 673 def DumpInstruments(self, forceUpdate: bool = True) -> str: 674 """ 675 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 676 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 677 678 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 679 680 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 681 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 682 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 683 """ 684 if self.iListDumpFile is None or not self.iListDumpFile: 685 uLogger.error("Output name of dump file must be defined!") 686 raise Exception("Filename required") 687 688 if not self.iList or forceUpdate: 689 self.iList = self.Listing() 690 691 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 692 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 693 fH.write(jsonDump) 694 695 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 696 697 return jsonDump 698 699 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 700 """ 701 Show information about one instrument defined by json data and prints it in Markdown format. 702 703 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 704 705 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 706 :param show: if `True` then also printing information about instrument and its current price. 707 :return: multilines text in Markdown format with information about one instrument. 708 """ 709 splitLine = "| | |\n" 710 infoText = "" 711 712 if iJSON is not None and iJSON and isinstance(iJSON, dict): 713 info = [ 714 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 715 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 716 "| Parameters | Values |\n", 717 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 718 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 719 "| Full name: | {:<54} |\n".format(iJSON["name"]), 720 ] 721 722 if "sector" in iJSON.keys() and iJSON["sector"]: 723 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 724 725 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 726 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 727 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 728 ))) 729 730 info.extend([ 731 splitLine, 732 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 733 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 734 ]) 735 736 if "isin" in iJSON.keys() and iJSON["isin"]: 737 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 738 739 if "classCode" in iJSON.keys(): 740 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 741 742 info.extend([ 743 splitLine, 744 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 745 splitLine, 746 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 747 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 748 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 749 ]) 750 751 if iJSON["figi"]: 752 self.figi = iJSON["figi"] 753 iJSON = iJSON | self.RequestTradingStatus() 754 755 info.extend([ 756 splitLine, 757 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 758 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 759 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 760 ]) 761 762 info.append(splitLine) 763 764 if "type" in iJSON.keys() and iJSON["type"]: 765 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 766 767 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 768 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 769 770 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 771 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 772 773 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 774 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 775 776 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 777 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 778 779 if "focusType" in iJSON.keys() and iJSON["focusType"]: 780 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 781 782 if "assetType" in iJSON.keys() and iJSON["assetType"]: 783 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 784 785 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 786 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 787 788 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 789 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 790 791 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 792 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 793 794 if "currency" in iJSON.keys(): 795 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 796 797 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 798 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 799 800 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 801 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 802 803 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 804 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 805 806 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 807 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 808 809 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 810 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 811 812 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 813 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 814 815 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 816 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 817 818 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 819 info.append("| Perpetual bond: | Yes |\n") 820 821 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 822 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 823 824 iExt = None 825 if iJSON["type"] == "Bonds": 826 info.extend([ 827 splitLine, 828 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 829 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 830 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 831 iJSON["nominal"]["currency"], 832 )), 833 ]) 834 835 if "floatingCouponFlag" in iJSON.keys(): 836 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 837 838 if "amortizationFlag" in iJSON.keys(): 839 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 840 841 info.append(splitLine) 842 843 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 844 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 845 846 if iJSON["figi"]: 847 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 848 849 info.extend([ 850 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 851 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 852 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 853 ]) 854 855 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 856 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 857 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 858 iJSON["aciValue"]["currency"] 859 ))) 860 861 if "currentPrice" in iJSON.keys(): 862 info.append(splitLine) 863 864 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 865 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 866 867 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 868 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 869 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 870 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 871 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 872 873 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 874 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 875 876 info.extend([ 877 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 878 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 879 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 880 )), 881 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 882 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 883 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 884 )), 885 "| Changes between last deal price and last close | {:<54} |\n".format( 886 "{:.2f}%{}".format( 887 iJSON["currentPrice"]["changes"], 888 " ({}{:.2f} {})".format( 889 "+" if bondChangesDelta > 0 else "", 890 bondChangesDelta, 891 aciCurrency 892 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 893 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 894 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 895 currency 896 ), 897 ) 898 ), 899 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 900 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 901 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 902 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 903 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 904 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 905 )), 906 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 907 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 908 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 909 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 910 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 911 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 912 )), 913 ]) 914 915 if "lot" in iJSON.keys(): 916 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 917 918 if "step" in iJSON.keys() and iJSON["step"] != 0: 919 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 920 921 # Add bond payment calendar: 922 if iJSON["type"] == "Bonds": 923 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 924 info.extend(["\n", strCalendar]) 925 926 infoText += "".join(info) 927 928 if show: 929 uLogger.info("{}".format(infoText)) 930 931 else: 932 uLogger.debug("{}".format(infoText)) 933 934 if self.infoFile is not None: 935 with open(self.infoFile, "w", encoding="UTF-8") as fH: 936 fH.write(infoText) 937 938 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 939 940 return infoText 941 942 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 943 """ 944 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 945 946 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 947 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 948 :return: JSON formatted data with information about instrument. 949 """ 950 tickerJSON = {} 951 if self.moreDebug: 952 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 953 954 if not self.ticker: 955 uLogger.warning("self.ticker variable is not be empty!") 956 957 else: 958 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 959 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 960 raise Exception("Instrument not allowed") 961 962 if not self.iList: 963 self.iList = self.Listing() 964 965 if self.ticker in self.iList["Shares"].keys(): 966 tickerJSON = self.iList["Shares"][self.ticker] 967 if self.moreDebug: 968 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 969 970 elif self.ticker in self.iList["Currencies"].keys(): 971 tickerJSON = self.iList["Currencies"][self.ticker] 972 if self.moreDebug: 973 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 974 975 elif self.ticker in self.iList["Bonds"].keys(): 976 tickerJSON = self.iList["Bonds"][self.ticker] 977 if self.moreDebug: 978 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 979 980 elif self.ticker in self.iList["Etfs"].keys(): 981 tickerJSON = self.iList["Etfs"][self.ticker] 982 if self.moreDebug: 983 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 984 985 elif self.ticker in self.iList["Futures"].keys(): 986 tickerJSON = self.iList["Futures"][self.ticker] 987 if self.moreDebug: 988 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 989 990 if tickerJSON: 991 self.figi = tickerJSON["figi"] 992 993 if requestPrice: 994 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 995 996 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 997 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 998 999 else: 1000 tickerJSON["currentPrice"]["changes"] = 0 1001 1002 if show: 1003 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1004 1005 else: 1006 if show: 1007 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1008 1009 return tickerJSON 1010 1011 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1012 """ 1013 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1014 1015 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1016 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1017 :return: JSON formatted data with information about instrument. 1018 """ 1019 figiJSON = {} 1020 if self.moreDebug: 1021 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1022 1023 if not self.figi: 1024 uLogger.warning("self.figi variable is not be empty!") 1025 1026 else: 1027 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1028 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1029 raise Exception("Instrument not allowed") 1030 1031 if not self.iList: 1032 self.iList = self.Listing() 1033 1034 for item in self.iList["Shares"].keys(): 1035 if self.figi == self.iList["Shares"][item]["figi"]: 1036 figiJSON = self.iList["Shares"][item] 1037 1038 if self.moreDebug: 1039 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1040 1041 break 1042 1043 if not figiJSON: 1044 for item in self.iList["Currencies"].keys(): 1045 if self.figi == self.iList["Currencies"][item]["figi"]: 1046 figiJSON = self.iList["Currencies"][item] 1047 1048 if self.moreDebug: 1049 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1050 1051 break 1052 1053 if not figiJSON: 1054 for item in self.iList["Bonds"].keys(): 1055 if self.figi == self.iList["Bonds"][item]["figi"]: 1056 figiJSON = self.iList["Bonds"][item] 1057 1058 if self.moreDebug: 1059 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1060 1061 break 1062 1063 if not figiJSON: 1064 for item in self.iList["Etfs"].keys(): 1065 if self.figi == self.iList["Etfs"][item]["figi"]: 1066 figiJSON = self.iList["Etfs"][item] 1067 1068 if self.moreDebug: 1069 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1070 1071 break 1072 1073 if not figiJSON: 1074 for item in self.iList["Futures"].keys(): 1075 if self.figi == self.iList["Futures"][item]["figi"]: 1076 figiJSON = self.iList["Futures"][item] 1077 1078 if self.moreDebug: 1079 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1080 1081 break 1082 1083 if figiJSON: 1084 self.figi = figiJSON["figi"] 1085 self.ticker = figiJSON["ticker"] 1086 1087 if requestPrice: 1088 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1089 1090 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1091 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1092 1093 else: 1094 figiJSON["currentPrice"]["changes"] = 0 1095 1096 if show: 1097 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1098 1099 else: 1100 if show: 1101 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1102 1103 return figiJSON 1104 1105 def GetCurrentPrices(self, show: bool = True) -> dict: 1106 """ 1107 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1108 `{"buy": [{"price": 1243.8, "quantity": 193}, 1109 {"price": 1244.0, "quantity": 168}, 1110 {"price": 1244.8, "quantity": 5}, 1111 {"price": 1245.0, "quantity": 61}, 1112 {"price": 1245.4, "quantity": 60}], 1113 "sell": [{"price": 1243.6, "quantity": 8}, 1114 {"price": 1242.6, "quantity": 10}, 1115 {"price": 1242.4, "quantity": 18}, 1116 {"price": 1242.2, "quantity": 50}, 1117 {"price": 1242.0, "quantity": 113}], 1118 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1119 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1120 - sell: list of dicts with Buyers prices, 1121 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1122 - quantity: volume value by current price in lots, 1123 - limitUp: current trade session limit price, maximum, 1124 - limitDown: current trade session limit price, minimum, 1125 - lastPrice: last deal price of the instrument, 1126 - closePrice: previous trade session close price of the instrument. 1127 1128 See also: `SearchByTicker()` and `SearchByFIGI()`. 1129 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1130 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1131 1132 :param show: if `True` then print DOM to log and console. 1133 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1134 If an error occurred then returns an empty record: 1135 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1136 """ 1137 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1138 1139 if self.depth < 1: 1140 uLogger.error("Depth of Market (DOM) must be >=1!") 1141 raise Exception("Incorrect value") 1142 1143 if not (self.ticker or self.figi): 1144 uLogger.error("self.ticker or self.figi variables must be defined!") 1145 raise Exception("Ticker or FIGI required") 1146 1147 if self.ticker and not self.figi: 1148 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1149 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1150 1151 if not self.ticker and self.figi: 1152 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1153 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1154 1155 if not self.figi: 1156 uLogger.error("FIGI is not defined!") 1157 raise Exception("Ticker or FIGI required") 1158 1159 else: 1160 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1161 1162 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1163 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1164 self.body = str({"figi": self.figi, "depth": self.depth}) 1165 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1166 1167 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1168 # list of dicts with sellers orders: 1169 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1170 1171 # list of dicts with buyers orders: 1172 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1173 1174 # max price of instrument at this time: 1175 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1176 1177 # min price of instrument at this time: 1178 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1179 1180 # last price of deal with instrument: 1181 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1182 1183 # last close price of instrument: 1184 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1185 1186 else: 1187 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1188 uLogger.debug("Server response: {}".format(pricesResponse)) 1189 1190 if show: 1191 if prices["buy"] or prices["sell"]: 1192 info = [ 1193 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1194 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1195 self.ticker, 1196 self.figi, 1197 self.depth, 1198 ), 1199 "-" * 60, "\n", 1200 " Orders of Buyers | Orders of Sellers\n", 1201 "-" * 60, "\n", 1202 " Sell prices (volumes) | Buy prices (volumes)\n", 1203 "-" * 60, "\n", 1204 ] 1205 1206 if not prices["buy"]: 1207 info.append(" | No orders!\n") 1208 sumBuy = 0 1209 1210 else: 1211 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1212 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1213 for item in maxMinSorted: 1214 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1215 1216 if not prices["sell"]: 1217 info.append("No orders! |\n") 1218 sumSell = 0 1219 1220 else: 1221 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1222 for item in prices["sell"]: 1223 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1224 1225 info.extend([ 1226 "-" * 60, "\n", 1227 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1228 "-" * 60, "\n", 1229 ]) 1230 1231 infoText = "".join(info) 1232 1233 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1234 1235 else: 1236 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1237 1238 return prices 1239 1240 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1241 """ 1242 This method get and show information about all available broker instruments for current user account. 1243 If `instrumentsFile` string is not empty then also save information to this file. 1244 1245 :param show: if `True` then print results to console, if `False` — print only to file. 1246 :return: multi-lines string with all available broker instruments 1247 """ 1248 if not self.iList: 1249 self.iList = self.Listing() 1250 1251 info = [ 1252 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1253 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1254 ] 1255 1256 # add instruments count by type: 1257 for iType in self.iList.keys(): 1258 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1259 1260 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1261 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1262 1263 # generating info tables with all instruments by type: 1264 for iType in self.iList.keys(): 1265 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1266 1267 for instrument in self.iList[iType].keys(): 1268 iName = self.iList[iType][instrument]["name"] # instrument's name 1269 if len(iName) > 57: 1270 iName = "{}...".format(iName[:54]) # right trim for a long string 1271 1272 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1273 self.iList[iType][instrument]["ticker"], 1274 iName, 1275 self.iList[iType][instrument]["figi"], 1276 self.iList[iType][instrument]["currency"], 1277 self.iList[iType][instrument]["lot"], 1278 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1279 )) 1280 1281 infoText = "".join(info) 1282 1283 if show: 1284 uLogger.info(infoText) 1285 1286 if self.instrumentsFile: 1287 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1288 fH.write(infoText) 1289 1290 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1291 1292 return infoText 1293 1294 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1295 """ 1296 This method search and show information about instruments by part of its ticker, FIGI or name. 1297 If `searchResultsFile` string is not empty then also save information to this file. 1298 1299 :param pattern: string with part of ticker, FIGI or instrument's name. 1300 :param show: if `True` then print results to console, if `False` — return list of result only. 1301 :return: list of dictionaries with all found instruments. 1302 """ 1303 if not self.iList: 1304 self.iList = self.Listing() 1305 1306 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1307 compiledPattern = re.compile(pattern, re.IGNORECASE) 1308 1309 for iType in self.iList: 1310 for instrument in self.iList[iType].values(): 1311 searchResult = compiledPattern.search(" ".join( 1312 [instrument["ticker"], instrument["figi"], instrument["name"]] 1313 )) 1314 1315 if searchResult: 1316 searchResults[iType][instrument["ticker"]] = instrument 1317 1318 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1319 info = [ 1320 "# Search results\n\n", 1321 "* **Search pattern:** [{}]\n".format(pattern), 1322 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1323 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1324 ] 1325 infoShort = info[:] 1326 1327 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1328 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1329 skippedLine = "| ... | ... | ... | ... |\n" 1330 1331 if resultsLen == 0: 1332 info.append("\nNo results\n") 1333 infoShort.append("\nNo results\n") 1334 uLogger.warning("No results. Try changing your search pattern.") 1335 1336 else: 1337 for iType in searchResults: 1338 iTypeValuesCount = len(searchResults[iType].values()) 1339 if iTypeValuesCount > 0: 1340 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1342 1343 for instrument in searchResults[iType].values(): 1344 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1345 instrument["type"], 1346 instrument["ticker"], 1347 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1348 instrument["figi"], 1349 )) 1350 1351 if iTypeValuesCount <= 5: 1352 infoShort.extend(info[-iTypeValuesCount:]) 1353 1354 else: 1355 infoShort.extend(info[-5:]) 1356 infoShort.append(skippedLine) 1357 1358 infoText = "".join(info) 1359 infoTextShort = "".join(infoShort) 1360 1361 if show: 1362 uLogger.info(infoTextShort) 1363 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1364 1365 if self.searchResultsFile: 1366 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1367 fH.write(infoText) 1368 1369 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1370 1371 return searchResults 1372 1373 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1374 """ 1375 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1376 1377 :param instruments: list of strings with tickers or FIGIs. 1378 :return: list with unique instrument FIGIs only. 1379 """ 1380 requestedInstruments = [] 1381 for iName in instruments: 1382 if iName not in self.aliases.keys(): 1383 if iName not in requestedInstruments: 1384 requestedInstruments.append(iName) 1385 1386 else: 1387 if iName not in requestedInstruments: 1388 if self.aliases[iName] not in requestedInstruments: 1389 requestedInstruments.append(self.aliases[iName]) 1390 1391 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1392 1393 onlyUniqueFIGIs = [] 1394 for iName in requestedInstruments: 1395 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1396 continue 1397 1398 self.ticker = iName 1399 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1400 1401 if not iData: 1402 self.ticker = "" 1403 self.figi = iName 1404 1405 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1406 1407 if not iData: 1408 self.figi = "" 1409 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1410 1411 if iData and iData["figi"] not in onlyUniqueFIGIs: 1412 onlyUniqueFIGIs.append(iData["figi"]) 1413 1414 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1415 1416 return onlyUniqueFIGIs 1417 1418 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1419 """ 1420 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1421 1422 See limits: https://tinkoff.github.io/investAPI/limits/ 1423 1424 If `pricesFile` string is not empty then also save information to this file. 1425 1426 :param instruments: list of strings with tickers or FIGIs. 1427 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1428 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1429 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1430 """ 1431 if instruments is None or not instruments: 1432 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1433 raise Exception("Ticker or FIGI required") 1434 1435 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1436 1437 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1438 1439 iList = [] # trying to get info and current prices about all unique instruments: 1440 for self.figi in onlyUniqueFIGIs: 1441 iData = self.SearchByFIGI(requestPrice=True) 1442 iList.append(iData) 1443 1444 self.ShowListOfPrices(iList, show) 1445 1446 return iList 1447 1448 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1449 """ 1450 Show table contains current prices of given instruments. 1451 1452 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1453 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1454 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1455 :return: multilines text in Markdown format as a table contains current prices. 1456 """ 1457 infoText = "" 1458 1459 if show or self.pricesFile: 1460 info = [ 1461 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1462 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1463 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1464 ] 1465 1466 for item in iList: 1467 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1468 item["ticker"], 1469 item["figi"], 1470 item["type"], 1471 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1472 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1473 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1474 "{} / {}".format( 1475 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1476 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1477 ), 1478 "{} / {}".format( 1479 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1480 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1481 ), 1482 item["currency"], 1483 )) 1484 1485 infoText = "".join(info) 1486 1487 if show: 1488 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1489 1490 if self.pricesFile: 1491 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1492 fH.write(infoText) 1493 1494 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1495 1496 return infoText 1497 1498 def RequestTradingStatus(self) -> dict: 1499 """ 1500 Requesting trading status for the instrument defined by `figi` variable. 1501 1502 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1503 1504 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1505 1506 :return: dictionary with trading status attributes. Response example: 1507 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1508 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1509 """ 1510 if self.figi is None or not self.figi: 1511 uLogger.error("Variable `figi` must be defined for using this method!") 1512 raise Exception("FIGI required") 1513 1514 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1515 1516 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1517 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1518 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1519 1520 if self.moreDebug: 1521 uLogger.debug("Records about current trading status successfully received") 1522 1523 return tradingStatus 1524 1525 def RequestPortfolio(self) -> dict: 1526 """ 1527 Requesting actual user's portfolio for current `accountId`. 1528 1529 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1530 1531 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1532 1533 :return: dictionary with user's portfolio. 1534 """ 1535 if self.accountId is None or not self.accountId: 1536 uLogger.error("Variable `accountId` must be defined for using this method!") 1537 raise Exception("Account ID required") 1538 1539 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1540 1541 self.body = str({"accountId": self.accountId}) 1542 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1543 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1544 1545 if self.moreDebug: 1546 uLogger.debug("Records about user's portfolio successfully received") 1547 1548 return rawPortfolio 1549 1550 def RequestPositions(self) -> dict: 1551 """ 1552 Requesting open positions by currencies and instruments for current `accountId`. 1553 1554 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1555 1556 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1557 1558 :return: dictionary with open positions by instruments. 1559 """ 1560 if self.accountId is None or not self.accountId: 1561 uLogger.error("Variable `accountId` must be defined for using this method!") 1562 raise Exception("Account ID required") 1563 1564 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1565 1566 self.body = str({"accountId": self.accountId}) 1567 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1568 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1569 1570 if self.moreDebug: 1571 uLogger.debug("Records about current open positions successfully received") 1572 1573 return rawPositions 1574 1575 def RequestPendingOrders(self) -> list: 1576 """ 1577 Requesting current actual pending orders for current `accountId`. 1578 1579 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1580 1581 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1582 1583 :return: list of dictionaries with pending orders. 1584 """ 1585 if self.accountId is None or not self.accountId: 1586 uLogger.error("Variable `accountId` must be defined for using this method!") 1587 raise Exception("Account ID required") 1588 1589 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1590 1591 self.body = str({"accountId": self.accountId}) 1592 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1593 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1594 1595 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1596 1597 return rawOrders 1598 1599 def RequestStopOrders(self) -> list: 1600 """ 1601 Requesting current actual stop orders for current `accountId`. 1602 1603 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1604 1605 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1606 1607 :return: list of dictionaries with stop orders. 1608 """ 1609 if self.accountId is None or not self.accountId: 1610 uLogger.error("Variable `accountId` must be defined for using this method!") 1611 raise Exception("Account ID required") 1612 1613 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1614 1615 self.body = str({"accountId": self.accountId}) 1616 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1617 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1618 1619 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1620 1621 return rawStopOrders 1622 1623 def Overview(self, show: bool = False, details: str = "full") -> dict: 1624 """ 1625 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1626 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1627 and `overviewBondsCalendarFile` are defined then also save information to file. 1628 1629 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1630 many requests about the state of the portfolio, and then, based on the received data, a large number 1631 of calculation and statistics are collected. 1632 1633 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1634 :param details: how detailed should the information be? 1635 - `full` — shows full available information about portfolio status (by default), 1636 - `positions` — shows only open positions, 1637 - `orders` — shows only sections of open limits and stop orders. 1638 - `digest` — show a short digest of the portfolio status, 1639 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1640 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1641 :return: dictionary with client's raw portfolio and some statistics. 1642 """ 1643 if self.accountId is None or not self.accountId: 1644 uLogger.error("Variable `accountId` must be defined for using this method!") 1645 raise Exception("Account ID required") 1646 1647 view = { 1648 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1649 "headers": {}, # list of dictionaries, response headers without "positions" section 1650 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1651 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1652 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1653 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1654 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1655 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1656 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1657 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1658 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1659 }, 1660 "stat": { # --- some statistics calculated using "raw" sections: 1661 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1662 "availableRUB": 0., # available rubles (without other currencies) 1663 "blockedRUB": 0., # blocked sum in Russian Rouble 1664 "totalChangesRUB": 0., # changes for all open trades in RUB 1665 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1666 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1667 "sharesCostRUB": 0., # costs of all shares in RUB 1668 "bondsCostRUB": 0., # costs of all bonds in RUB 1669 "etfsCostRUB": 0., # costs of all etfs in RUB 1670 "futuresCostRUB": 0., # costs of all futures in RUB 1671 "Currencies": [], # list of dictionaries of all currencies statistics 1672 "Shares": [], # list of dictionaries of all shares statistics 1673 "Bonds": [], # list of dictionaries of all bonds statistics 1674 "Etfs": [], # list of dictionaries of all etfs statistics 1675 "Futures": [], # list of dictionaries of all futures statistics 1676 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1677 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1678 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1679 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1680 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1681 }, 1682 "analytics": { # --- some analytics of portfolio: 1683 "distrByAssets": {}, # portfolio distribution by assets 1684 "distrByCompanies": {}, # portfolio distribution by companies 1685 "distrBySectors": {}, # portfolio distribution by sectors 1686 "distrByCurrencies": {}, # portfolio distribution by currencies 1687 "distrByCountries": {}, # portfolio distribution by countries 1688 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1689 } 1690 } 1691 1692 details = details.lower() 1693 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1694 if details not in availableDetails: 1695 details = "full" 1696 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1697 1698 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1699 1700 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1701 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1702 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1703 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1704 1705 # save response headers without "positions" section: 1706 for key in portfolioResponse.keys(): 1707 if key != "positions": 1708 view["raw"]["headers"][key] = portfolioResponse[key] 1709 1710 else: 1711 continue 1712 1713 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1714 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1715 for item in portfolioResponse["positions"]: 1716 if item["instrumentType"] == "currency": 1717 self.figi = item["figi"] 1718 curr = self.SearchByFIGI(requestPrice=False) 1719 1720 # current price of currency in RUB: 1721 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1722 "name": curr["name"], 1723 "currentPrice": NanoToFloat( 1724 item["currentPrice"]["units"], 1725 item["currentPrice"]["nano"] 1726 ), 1727 } 1728 1729 view["raw"]["Currencies"].append(item) 1730 1731 elif item["instrumentType"] == "share": 1732 view["raw"]["Shares"].append(item) 1733 1734 elif item["instrumentType"] == "bond": 1735 view["raw"]["Bonds"].append(item) 1736 1737 elif item["instrumentType"] == "etf": 1738 view["raw"]["Etfs"].append(item) 1739 1740 elif item["instrumentType"] == "futures": 1741 view["raw"]["Futures"].append(item) 1742 1743 else: 1744 continue 1745 1746 # how many volume of currencies (by ISO currency name) are blocked: 1747 for item in view["raw"]["positions"]["blocked"]: 1748 blocked = NanoToFloat(item["units"], item["nano"]) 1749 if blocked > 0: 1750 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1751 1752 # how many volume of instruments (by FIGI) are blocked: 1753 for item in view["raw"]["positions"]["securities"]: 1754 blocked = int(item["blocked"]) 1755 if blocked > 0: 1756 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1757 1758 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1759 1760 if "rub" in allBlocked.keys(): 1761 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1762 1763 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1764 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1765 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1766 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1767 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1768 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1769 view["stat"]["portfolioCostRUB"] = sum([ 1770 view["stat"]["allCurrenciesCostRUB"], 1771 view["stat"]["sharesCostRUB"], 1772 view["stat"]["bondsCostRUB"], 1773 view["stat"]["etfsCostRUB"], 1774 view["stat"]["futuresCostRUB"], 1775 ]) 1776 1777 # --- calculating some portfolio statistics: 1778 byComp = {} # distribution by companies 1779 bySect = {} # distribution by sectors 1780 byCurr = {} # distribution by currencies (include RUB) 1781 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1782 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1783 1784 for item in portfolioResponse["positions"]: 1785 self.figi = item["figi"] 1786 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1787 1788 if instrument: 1789 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1790 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1791 1792 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1793 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1794 1795 else: 1796 blocked = 0 1797 1798 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1799 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1800 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1801 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1802 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1803 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1804 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1805 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1806 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1807 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1808 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1809 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1810 1811 statData = { 1812 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1813 "ticker": instrument["ticker"], # ticker by FIGI 1814 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1815 "volume": volume, # available volume of instrument 1816 "lots": lots, # volume in lots of instrument 1817 "direction": direction, # direction of an instrument's position: short or long 1818 "blocked": blocked, # blocked volume of currency or instrument 1819 "currentPrice": curPrice, # current instrument's price in basic asset 1820 "average": average, # current average position price 1821 "cost": cost, # current cost of all volume of instrument in basic asset 1822 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1823 "costRUB": costRUB, # cost of instrument in ruble 1824 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1825 "profit": profit, # expected profit at current moment 1826 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1827 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1828 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1829 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1830 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1831 "step": instrument["step"], # minimum price increment 1832 } 1833 1834 # adding distribution by unique countries: 1835 if statData["country"] not in byCountry.keys(): 1836 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 byCountry[statData["country"]]["cost"] += costRUB 1840 byCountry[statData["country"]]["percent"] += percentCostRUB 1841 1842 if item["instrumentType"] != "currency": 1843 # adding distribution by unique companies: 1844 if statData["name"]: 1845 if statData["name"] not in byComp.keys(): 1846 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1847 1848 else: 1849 byComp[statData["name"]]["cost"] += costRUB 1850 byComp[statData["name"]]["percent"] += percentCostRUB 1851 1852 # adding distribution by unique sectors: 1853 if statData["sector"] not in bySect.keys(): 1854 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1855 1856 else: 1857 bySect[statData["sector"]]["cost"] += costRUB 1858 bySect[statData["sector"]]["percent"] += percentCostRUB 1859 1860 # adding distribution by unique currencies: 1861 if currency not in byCurr.keys(): 1862 byCurr[currency] = { 1863 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1864 "cost": costRUB, 1865 "percent": percentCostRUB 1866 } 1867 1868 else: 1869 byCurr[currency]["cost"] += costRUB 1870 byCurr[currency]["percent"] += percentCostRUB 1871 1872 # saving statistics for every instrument: 1873 if item["instrumentType"] == "currency": 1874 view["stat"]["Currencies"].append(statData) 1875 1876 # update dict with free funds for trading (total - blocked) by currencies 1877 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1878 view["stat"]["funds"][currency] = { 1879 "total": volume, 1880 "totalCostRUB": costRUB, # total volume cost in rubles 1881 "free": volume - blocked, 1882 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1883 } 1884 1885 elif item["instrumentType"] == "share": 1886 view["stat"]["Shares"].append(statData) 1887 1888 elif item["instrumentType"] == "bond": 1889 view["stat"]["Bonds"].append(statData) 1890 1891 elif item["instrumentType"] == "etf": 1892 view["stat"]["Etfs"].append(statData) 1893 1894 elif item["instrumentType"] == "Futures": 1895 view["stat"]["Futures"].append(statData) 1896 1897 else: 1898 continue 1899 1900 # total changes in Russian Ruble: 1901 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1902 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1903 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1904 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1905 view["stat"]["funds"]["rub"] = { 1906 "total": view["stat"]["availableRUB"], 1907 "totalCostRUB": view["stat"]["availableRUB"], 1908 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1909 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1910 } 1911 1912 # --- pending orders sector data: 1913 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1914 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1915 1916 for item in view["raw"]["orders"]: 1917 self.figi = item["figi"] 1918 1919 if item["figi"] not in uniquePendingOrdersFIGIs: 1920 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1921 1922 uniquePendingOrdersFIGIs.append(item["figi"]) 1923 uniquePendingOrders[item["figi"]] = instrument 1924 1925 else: 1926 instrument = uniquePendingOrders[item["figi"]] 1927 1928 if instrument: 1929 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1930 orderType = TKS_ORDER_TYPES[item["orderType"]] 1931 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1932 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1933 1934 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1935 if item["direction"] == "ORDER_DIRECTION_BUY": 1936 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1937 1938 else: 1939 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1940 1941 # requested price for order execution: 1942 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1943 1944 # necessary changes in percent to reach target from current price: 1945 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1946 1947 view["stat"]["orders"].append({ 1948 "orderID": item["orderId"], # orderId number parameter of current order 1949 "figi": item["figi"], # FIGI identification 1950 "ticker": instrument["ticker"], # ticker name by FIGI 1951 "lotsRequested": item["lotsRequested"], # requested lots value 1952 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1953 "currentPrice": lastPrice, # current instrument's price for defined action 1954 "targetPrice": target, # requested price for order execution in base currency 1955 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1956 "percentChanges": changes, # changes in percent to target from current price 1957 "currency": item["currency"], # instrument's currency name 1958 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1959 "type": orderType, # type of order from TKS_ORDER_TYPES 1960 "status": orderState, # order status from TKS_ORDER_STATES 1961 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1962 }) 1963 1964 # --- stop orders sector data: 1965 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1966 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1967 1968 for item in view["raw"]["stopOrders"]: 1969 self.figi = item["figi"] 1970 1971 if item["figi"] not in uniqueStopOrdersFIGIs: 1972 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1973 1974 uniqueStopOrdersFIGIs.append(item["figi"]) 1975 uniqueStopOrders[item["figi"]] = instrument 1976 1977 else: 1978 instrument = uniqueStopOrders[item["figi"]] 1979 1980 if instrument: 1981 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1982 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1983 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1984 1985 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1986 if "expirationTime" in item.keys(): 1987 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1988 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1989 1990 else: 1991 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1992 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1993 1994 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1995 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1996 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1997 1998 else: 1999 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2000 2001 # requested price when stop-order executed: 2002 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2003 2004 # price for limit-order, set up when stop-order executed: 2005 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2006 2007 # necessary changes in percent to reach target from current price: 2008 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2009 2010 view["stat"]["stopOrders"].append({ 2011 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2012 "figi": item["figi"], # FIGI identification 2013 "ticker": instrument["ticker"], # ticker name by FIGI 2014 "lotsRequested": item["lotsRequested"], # requested lots value 2015 "currentPrice": lastPrice, # current instrument's price for defined action 2016 "targetPrice": target, # requested price for stop-order execution in base currency 2017 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2018 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2019 "percentChanges": changes, # changes in percent to target from current price 2020 "currency": item["currency"], # instrument's currency name 2021 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2022 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2023 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2024 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2025 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2026 }) 2027 2028 # --- calculating data for analytics section: 2029 # portfolio distribution by assets: 2030 view["analytics"]["distrByAssets"] = { 2031 "Ruble": { 2032 "uniques": 1, 2033 "cost": view["stat"]["availableRUB"], 2034 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2035 }, 2036 "Currencies": { 2037 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2038 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2039 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2040 }, 2041 "Shares": { 2042 "uniques": len(view["stat"]["Shares"]), 2043 "cost": view["stat"]["sharesCostRUB"], 2044 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2045 }, 2046 "Bonds": { 2047 "uniques": len(view["stat"]["Bonds"]), 2048 "cost": view["stat"]["bondsCostRUB"], 2049 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2050 }, 2051 "Etfs": { 2052 "uniques": len(view["stat"]["Etfs"]), 2053 "cost": view["stat"]["etfsCostRUB"], 2054 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2055 }, 2056 "Futures": { 2057 "uniques": len(view["stat"]["Futures"]), 2058 "cost": view["stat"]["futuresCostRUB"], 2059 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2060 }, 2061 } 2062 2063 # portfolio distribution by companies: 2064 view["analytics"]["distrByCompanies"]["All money cash"] = { 2065 "ticker": "", 2066 "cost": view["stat"]["allCurrenciesCostRUB"], 2067 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2068 } 2069 view["analytics"]["distrByCompanies"].update(byComp) 2070 2071 # portfolio distribution by sectors: 2072 view["analytics"]["distrBySectors"]["All money cash"] = { 2073 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2074 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2075 } 2076 view["analytics"]["distrBySectors"].update(bySect) 2077 2078 # portfolio distribution by currencies: 2079 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2080 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2081 2082 if self.moreDebug: 2083 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2084 2085 view["analytics"]["distrByCurrencies"].update(byCurr) 2086 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2087 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2088 2089 # portfolio distribution by countries: 2090 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2091 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2092 2093 if self.moreDebug: 2094 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2095 2096 view["analytics"]["distrByCountries"].update(byCountry) 2097 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2098 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2099 2100 # --- Prepare text statistics overview in human-readable: 2101 if show: 2102 # Whatever the value `details`, header not changes: 2103 info = [ 2104 "# Client's portfolio\n\n", 2105 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2106 "* **Account ID:** [{}]\n".format(self.accountId), 2107 ] 2108 2109 if details in ["full", "positions", "digest"]: 2110 info.extend([ 2111 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2112 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2113 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2114 view["stat"]["totalChangesRUB"], 2115 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2116 view["stat"]["totalChangesPercentRUB"], 2117 ), 2118 ]) 2119 2120 if details in ["full", "positions"]: 2121 info.extend([ 2122 "## Open positions\n\n", 2123 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2124 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2125 "| Ruble | {:>31} | | | | | |\n".format( 2126 "{:.2f} ({:.2f}) rub".format( 2127 view["stat"]["availableRUB"], 2128 view["stat"]["blockedRUB"], 2129 ) 2130 ) 2131 ]) 2132 2133 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2134 return [ 2135 "| | | | | | | |\n", 2136 "| {:<27} | | | | | {:>19} | |\n".format( 2137 noTradeStr if noTradeStr else typeStr, 2138 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2139 ), 2140 ] 2141 2142 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2143 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2144 "{} [{}]".format(data["ticker"], data["figi"]), 2145 "{:.2f} ({:.2f}) {}".format( 2146 data["volume"], 2147 data["blocked"], 2148 data["currency"], 2149 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2150 data["volume"], 2151 data["blocked"], 2152 ), 2153 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2154 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2155 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2156 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2157 "{}{:.2f} {} ({}{:.2f}%)".format( 2158 "+" if data["profit"] > 0 else "", 2159 data["profit"], data["baseCurrencyName"], 2160 "+" if data["percentProfit"] > 0 else "", 2161 data["percentProfit"], 2162 ), 2163 ) 2164 2165 # --- Show currencies section: 2166 if view["stat"]["Currencies"]: 2167 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2168 for item in view["stat"]["Currencies"]: 2169 info.append(_InfoStr(item, showCurrencyName=True)) 2170 2171 else: 2172 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2173 2174 # --- Show shares section: 2175 if view["stat"]["Shares"]: 2176 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2177 2178 for item in view["stat"]["Shares"]: 2179 info.append(_InfoStr(item)) 2180 2181 else: 2182 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2183 2184 # --- Show bonds section: 2185 if view["stat"]["Bonds"]: 2186 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2187 2188 for item in view["stat"]["Bonds"]: 2189 info.append(_InfoStr(item)) 2190 2191 else: 2192 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2193 2194 # --- Show etfs section: 2195 if view["stat"]["Etfs"]: 2196 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2197 2198 for item in view["stat"]["Etfs"]: 2199 info.append(_InfoStr(item)) 2200 2201 else: 2202 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2203 2204 # --- Show futures section: 2205 if view["stat"]["Futures"]: 2206 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2207 2208 for item in view["stat"]["Futures"]: 2209 info.append(_InfoStr(item)) 2210 2211 else: 2212 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2213 2214 if details in ["full", "orders"]: 2215 # --- Show pending orders section: 2216 if view["stat"]["orders"]: 2217 info.extend([ 2218 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2219 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2220 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2221 ]) 2222 2223 for item in view["stat"]["orders"]: 2224 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2225 "{} [{}]".format(item["ticker"], item["figi"]), 2226 item["orderID"], 2227 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2228 "{} {} ({}{:.2f}%)".format( 2229 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2230 item["baseCurrencyName"], 2231 "+" if item["percentChanges"] > 0 else "", 2232 float(item["percentChanges"]), 2233 ), 2234 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2235 item["action"], 2236 item["type"], 2237 item["date"], 2238 )) 2239 2240 else: 2241 info.append("\n## Total pending limit-orders: 0\n") 2242 2243 # --- Show stop orders section: 2244 if view["stat"]["stopOrders"]: 2245 info.extend([ 2246 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2247 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2248 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2249 ]) 2250 2251 for item in view["stat"]["stopOrders"]: 2252 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2253 "{} [{}]".format(item["ticker"], item["figi"]), 2254 item["orderID"], 2255 item["lotsRequested"], 2256 "{} {} ({}{:.2f}%)".format( 2257 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2258 item["baseCurrencyName"], 2259 "+" if item["percentChanges"] > 0 else "", 2260 float(item["percentChanges"]), 2261 ), 2262 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2263 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2264 item["action"], 2265 item["type"], 2266 item["expType"], 2267 item["createDate"], 2268 item["expDate"], 2269 )) 2270 2271 else: 2272 info.append("\n## Total stop-orders: 0\n") 2273 2274 if details in ["full", "analytics"]: 2275 # -- Show analytics section: 2276 if view["stat"]["portfolioCostRUB"] > 0: 2277 info.extend([ 2278 "\n# Analytics\n" 2279 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2280 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2281 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2282 view["stat"]["totalChangesRUB"], 2283 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2284 view["stat"]["totalChangesPercentRUB"], 2285 ), 2286 "\n## Portfolio distribution by assets\n" 2287 "\n| Type | Uniques | Percent | Current cost |\n", 2288 "|------------------------------------|---------|---------|--------------------|\n", 2289 ]) 2290 2291 for key in view["analytics"]["distrByAssets"].keys(): 2292 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2293 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2294 key, 2295 view["analytics"]["distrByAssets"][key]["uniques"], 2296 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2297 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2298 )) 2299 2300 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2301 2302 info.extend([ 2303 "\n## Portfolio distribution by companies\n" 2304 "\n| Company | Percent | Current cost |\n", 2305 aSepLine, 2306 ]) 2307 2308 for company in view["analytics"]["distrByCompanies"].keys(): 2309 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2310 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2311 "{}{}".format( 2312 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2313 company, 2314 ), 2315 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2316 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2317 )) 2318 2319 info.extend([ 2320 "\n## Portfolio distribution by sectors\n" 2321 "\n| Sector | Percent | Current cost |\n", 2322 aSepLine, 2323 ]) 2324 2325 for sector in view["analytics"]["distrBySectors"].keys(): 2326 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2327 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2328 sector, 2329 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2330 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2331 )) 2332 2333 info.extend([ 2334 "\n## Portfolio distribution by currencies\n" 2335 "\n| Instruments currencies | Percent | Current cost |\n", 2336 aSepLine, 2337 ]) 2338 2339 for curr in view["analytics"]["distrByCurrencies"].keys(): 2340 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2341 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2342 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2343 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2344 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2345 )) 2346 2347 info.extend([ 2348 "\n## Portfolio distribution by countries\n" 2349 "\n| Assets by country | Percent | Current cost |\n", 2350 aSepLine, 2351 ]) 2352 2353 for country in view["analytics"]["distrByCountries"].keys(): 2354 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2355 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2356 country, 2357 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2358 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2359 )) 2360 2361 if details in ["full", "calendar"]: 2362 # -- Show bonds payment calendar section: 2363 if view["stat"]["Bonds"]: 2364 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2365 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2366 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2367 2368 else: 2369 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2370 2371 infoText = "".join(info) 2372 2373 uLogger.info(infoText) 2374 2375 if details == "full" and self.overviewFile: 2376 filename = self.overviewFile 2377 2378 elif details == "digest" and self.overviewDigestFile: 2379 filename = self.overviewDigestFile 2380 2381 elif details == "positions" and self.overviewPositionsFile: 2382 filename = self.overviewPositionsFile 2383 2384 elif details == "orders" and self.overviewOrdersFile: 2385 filename = self.overviewOrdersFile 2386 2387 elif details == "analytics" and self.overviewAnalyticsFile: 2388 filename = self.overviewAnalyticsFile 2389 2390 elif details == "calendar" and self.overviewBondsCalendarFile: 2391 filename = self.overviewBondsCalendarFile 2392 2393 else: 2394 filename = "" 2395 2396 if filename: 2397 with open(filename, "w", encoding="UTF-8") as fH: 2398 fH.write(infoText) 2399 2400 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2401 2402 return view 2403 2404 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2405 """ 2406 Returns history operations between two given dates for current `accountId`. 2407 If `reportFile` string is not empty then also save human-readable report. 2408 Shows some statistical data of closed positions. 2409 2410 :param start: see docstring in `GetDatesAsString()` method 2411 :param end: see docstring in `GetDatesAsString()` method 2412 :param show: if `True` then also prints all records to the console. 2413 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2414 :return: original list of dictionaries with history of deals records from API ("operations" key): 2415 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2416 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2417 """ 2418 if self.accountId is None or not self.accountId: 2419 uLogger.error("Variable `accountId` must be defined for using this method!") 2420 raise Exception("Account ID required") 2421 2422 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2423 2424 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2425 2426 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2427 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2428 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2429 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2430 customStat = {} # custom statistics in additional to responseJSON 2431 2432 # --- output report in human-readable format: 2433 if show or self.reportFile: 2434 splitLine1 = "| | | | | |\n" # Summary section 2435 splitLine2 = "| | | | | | | | |\n" # Operations section 2436 nextDay = "" 2437 2438 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2439 2440 if len(ops) > 0: 2441 customStat = { 2442 "opsCount": 0, # total operations count 2443 "buyCount": 0, # buy operations 2444 "sellCount": 0, # sell operations 2445 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2446 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2447 "payIn": {"rub": 0.}, # Deposit brokerage account 2448 "payOut": {"rub": 0.}, # Withdrawals 2449 "divs": {"rub": 0.}, # Dividends income 2450 "coupons": {"rub": 0.}, # Coupon's income 2451 "brokerCom": {"rub": 0.}, # Service commissions 2452 "serviceCom": {"rub": 0.}, # Service commissions 2453 "marginCom": {"rub": 0.}, # Margin commissions 2454 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2455 } 2456 2457 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2458 for item in ops: 2459 if item["state"] == "OPERATION_STATE_EXECUTED": 2460 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2461 2462 # count buy operations: 2463 if "_BUY" in item["operationType"]: 2464 customStat["buyCount"] += 1 2465 2466 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2467 customStat["buyTotal"][item["payment"]["currency"]] += payment 2468 2469 else: 2470 customStat["buyTotal"][item["payment"]["currency"]] = payment 2471 2472 # count sell operations: 2473 elif "_SELL" in item["operationType"]: 2474 customStat["sellCount"] += 1 2475 2476 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2477 customStat["sellTotal"][item["payment"]["currency"]] += payment 2478 2479 else: 2480 customStat["sellTotal"][item["payment"]["currency"]] = payment 2481 2482 # count incoming operations: 2483 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2484 if item["payment"]["currency"] in customStat["payIn"].keys(): 2485 customStat["payIn"][item["payment"]["currency"]] += payment 2486 2487 else: 2488 customStat["payIn"][item["payment"]["currency"]] = payment 2489 2490 # count withdrawals operations: 2491 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2492 if item["payment"]["currency"] in customStat["payOut"].keys(): 2493 customStat["payOut"][item["payment"]["currency"]] += payment 2494 2495 else: 2496 customStat["payOut"][item["payment"]["currency"]] = payment 2497 2498 # count dividends income: 2499 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2500 if item["payment"]["currency"] in customStat["divs"].keys(): 2501 customStat["divs"][item["payment"]["currency"]] += payment 2502 2503 else: 2504 customStat["divs"][item["payment"]["currency"]] = payment 2505 2506 # count coupon's income: 2507 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2508 if item["payment"]["currency"] in customStat["coupons"].keys(): 2509 customStat["coupons"][item["payment"]["currency"]] += payment 2510 2511 else: 2512 customStat["coupons"][item["payment"]["currency"]] = payment 2513 2514 # count broker commissions: 2515 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2516 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2517 customStat["brokerCom"][item["payment"]["currency"]] += payment 2518 2519 else: 2520 customStat["brokerCom"][item["payment"]["currency"]] = payment 2521 2522 # count service commissions: 2523 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2524 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2525 customStat["serviceCom"][item["payment"]["currency"]] += payment 2526 2527 else: 2528 customStat["serviceCom"][item["payment"]["currency"]] = payment 2529 2530 # count margin commissions: 2531 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2532 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2533 customStat["marginCom"][item["payment"]["currency"]] += payment 2534 2535 else: 2536 customStat["marginCom"][item["payment"]["currency"]] = payment 2537 2538 # count withholding taxes: 2539 elif "_TAX" in item["operationType"]: 2540 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2541 customStat["allTaxes"][item["payment"]["currency"]] += payment 2542 2543 else: 2544 customStat["allTaxes"][item["payment"]["currency"]] = payment 2545 2546 else: 2547 continue 2548 2549 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2550 2551 # --- view "Actions" lines: 2552 info.extend([ 2553 "| Report sections | | | | |\n", 2554 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2555 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2556 "| | Buy: {:<22} | {:<28} | | |\n".format( 2557 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2558 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2559 ), 2560 "| | Sell: {:<21} | {:<28} | | |\n".format( 2561 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2562 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2563 ), 2564 ]) 2565 2566 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2567 for key in opsKeys: 2568 if key == "rub": 2569 continue 2570 2571 info.extend([ 2572 "| | | {:<28} | | |\n".format( 2573 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2574 ), 2575 "| | | {:<28} | | |\n".format( 2576 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2577 ), 2578 ]) 2579 2580 info.append(splitLine1) 2581 2582 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2583 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2584 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2585 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2586 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2587 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2588 ) 2589 2590 # --- view "Payments" lines: 2591 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2592 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2593 2594 for key in paymentsKeys: 2595 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2596 2597 info.append(splitLine1) 2598 2599 # --- view "Commissions and taxes" lines: 2600 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2601 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2602 2603 for key in comKeys: 2604 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2605 2606 info.append(splitLine1) 2607 2608 info.extend([ 2609 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2610 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2611 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2612 ]) 2613 2614 else: 2615 info.append("Broker returned no operations during this period\n") 2616 2617 # --- view "Operations" section: 2618 for item in ops: 2619 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2620 continue 2621 2622 else: 2623 self.figi = item["figi"] if item["figi"] else "" 2624 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2625 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2626 2627 # group of deals during one day: 2628 if nextDay and item["date"].split("T")[0] != nextDay: 2629 info.append(splitLine2) 2630 nextDay = "" 2631 2632 else: 2633 nextDay = item["date"].split("T")[0] # saving current day for splitting 2634 2635 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2636 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2637 self.figi if self.figi else "—", 2638 instrument["ticker"] if instrument else "—", 2639 instrument["type"] if instrument else "—", 2640 item["quantity"] if int(item["quantity"]) > 0 else "—", 2641 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2642 TKS_OPERATION_STATES[item["state"]], 2643 TKS_OPERATION_TYPES[item["operationType"]], 2644 )) 2645 2646 infoText = "".join(info) 2647 2648 if show: 2649 if self.moreDebug: 2650 uLogger.debug("Records about history of a client's operations successfully received") 2651 2652 uLogger.info(infoText) 2653 2654 if self.reportFile: 2655 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2656 fH.write(infoText) 2657 2658 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2659 2660 return ops, customStat 2661 2662 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2663 """ 2664 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2665 2666 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2667 Warning! Broker server used ISO UTC time by default. 2668 2669 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2670 Also, `historyFile` used to update history with `onlyMissing` parameter. 2671 2672 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2673 2674 :param start: see docstring in `GetDatesAsString()` method. 2675 :param end: see docstring in `GetDatesAsString()` method. 2676 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2677 `"hour"`, `"day"`. Default: `"hour"`. 2678 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2679 False by default. Warning! History appends only from last candle to current time 2680 with always update last candle! 2681 :param csvSep: separator if csv-file is used, `,` by default. 2682 :param show: if `True` then also prints Pandas DataFrame to the console. 2683 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2684 `["date", "time", "open", "high", "low", "close", "volume"]`. 2685 """ 2686 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2687 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2688 history = None # empty pandas object for history 2689 2690 if interval not in TKS_CANDLE_INTERVALS.keys(): 2691 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2692 raise Exception("Incorrect value") 2693 2694 if not (self.ticker or self.figi): 2695 uLogger.error("Ticker or FIGI must be defined!") 2696 raise Exception("Ticker or FIGI required") 2697 2698 if self.ticker and not self.figi: 2699 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2700 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2701 2702 if self.figi and not self.ticker: 2703 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2704 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2705 2706 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2707 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2708 if interval.lower() != "day": 2709 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2710 2711 delta = dtEnd - dtStart # current UTC time minus last time in file 2712 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2713 2714 # calculate history length in candles: 2715 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2716 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2717 length += 1 # to avoid fraction time 2718 2719 # calculate data blocks count: 2720 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2721 2722 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2723 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2724 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2725 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2726 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2727 2728 tempOld = None # pandas object for old history, if --only-missing key present 2729 lastTime = None # datetime object of last old candle in file 2730 2731 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2732 uLogger.debug("--only-missing key present, add only last missing candles...") 2733 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2734 2735 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2736 2737 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2738 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2739 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2740 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2741 2742 # get last datetime object from last string in file or minus 1 delta if file is empty: 2743 if len(tempOld) > 0: 2744 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2745 2746 else: 2747 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2748 2749 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2750 2751 responseJSONs = [] # raw history blocks of data 2752 2753 blockEnd = dtEnd 2754 for item in range(blocks): 2755 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2756 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2757 2758 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2759 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2760 )) 2761 2762 if blockStart == blockEnd: 2763 uLogger.debug("Skipped this zero-length block...") 2764 2765 else: 2766 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2767 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2768 self.body = str({ 2769 "figi": self.figi, 2770 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2771 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2772 "interval": TKS_CANDLE_INTERVALS[interval][0] 2773 }) 2774 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2775 2776 if "code" in responseJSON.keys(): 2777 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2778 2779 else: 2780 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2781 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2782 2783 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2784 2785 blockEnd = blockStart 2786 2787 printCount = len(responseJSONs) # candles to show in console 2788 if responseJSONs: 2789 tempHistory = pd.DataFrame( 2790 data={ 2791 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2792 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2793 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2794 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2795 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2796 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2797 "volume": [int(item["volume"]) for item in responseJSONs], 2798 }, 2799 index=range(len(responseJSONs)), 2800 columns=["date", "time", "open", "high", "low", "close", "volume"], 2801 ) 2802 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2803 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2804 2805 # append only newest candles to old history if --only-missing key present: 2806 if onlyMissing and tempOld is not None and lastTime is not None: 2807 index = 0 # find start index in tempHistory data: 2808 2809 for i, item in tempHistory.iterrows(): 2810 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2811 2812 if curTime == lastTime: 2813 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2814 index = i 2815 printCount = index + 1 2816 break 2817 2818 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2819 2820 else: 2821 history = tempHistory # if no `--only-missing` key then load full data from server 2822 2823 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2824 2825 if history is not None and not history.empty: 2826 if show: 2827 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2828 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2829 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2830 )) 2831 2832 else: 2833 uLogger.warning("Received an empty candles history!") 2834 2835 if self.historyFile is not None: 2836 if history is not None and not history.empty: 2837 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2838 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2839 2840 else: 2841 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2842 2843 else: 2844 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2845 2846 return history 2847 2848 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2849 """ 2850 Load candles history from csv-file and return Pandas DataFrame object. 2851 2852 See also: `History()` and `ShowHistoryChart()` methods. 2853 2854 :param filePath: path to csv-file to open. 2855 """ 2856 loadedHistory = None # init candles data object 2857 2858 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2859 2860 if os.path.exists(filePath): 2861 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2862 2863 tfStr = self.priceModel.FormattedDelta( 2864 self.priceModel.timeframe, 2865 "{days} days {hours}h {minutes}m {seconds}s", 2866 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2867 self.priceModel.timeframe, 2868 "{hours}h {minutes}m {seconds}s", 2869 ) 2870 2871 if loadedHistory is not None and not loadedHistory.empty: 2872 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2873 len(loadedHistory), 2874 tfStr, 2875 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2876 ) 2877 2878 else: 2879 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2880 2881 else: 2882 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2883 2884 return loadedHistory 2885 2886 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2887 """ 2888 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2889 2890 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2891 Default: `index.html` (both for interact and non-interact candlesticks chart). 2892 2893 See also: `History()` and `LoadHistory()` methods. 2894 2895 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2896 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2897 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2898 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2899 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2900 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2901 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2902 """ 2903 if isinstance(candles, str): 2904 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2905 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2906 2907 elif isinstance(candles, pd.DataFrame): 2908 self.priceModel.prices = candles # set candles chain from variable 2909 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2910 2911 if "datetime" not in candles.columns: 2912 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2913 2914 else: 2915 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2916 raise Exception("Incorrect value") 2917 2918 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2919 2920 if interact: 2921 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2922 2923 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2924 2925 else: 2926 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2927 2928 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2929 2930 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2931 2932 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2933 """ 2934 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2935 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2936 2937 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2938 2939 :param operation: string "Buy" or "Sell". 2940 :param lots: volume, integer count of lots >= 1. 2941 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2942 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2943 :param expDate: string "Undefined" by default or local date in future, 2944 it is a string with format `%Y-%m-%d %H:%M:%S`. 2945 :return: JSON with response from broker server. 2946 """ 2947 if self.accountId is None or not self.accountId: 2948 uLogger.error("Variable `accountId` must be defined for using this method!") 2949 raise Exception("Account ID required") 2950 2951 if operation is None or not operation or operation not in ("Buy", "Sell"): 2952 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2953 raise Exception("Incorrect value") 2954 2955 if lots is None or lots < 1: 2956 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2957 lots = 1 2958 2959 if tp is None or tp < 0: 2960 tp = 0 2961 2962 if sl is None or sl < 0: 2963 sl = 0 2964 2965 if expDate is None or not expDate: 2966 expDate = "Undefined" 2967 2968 if not (self.ticker or self.figi): 2969 uLogger.error("Ticker or FIGI must be defined!") 2970 raise Exception("Ticker or FIGI required") 2971 2972 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2973 self.ticker = instrument["ticker"] 2974 self.figi = instrument["figi"] 2975 2976 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2977 2978 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2979 self.body = str({ 2980 "figi": self.figi, 2981 "quantity": str(lots), 2982 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2983 "accountId": str(self.accountId), 2984 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2985 }) 2986 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2987 2988 if "orderId" in response.keys(): 2989 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2990 operation, response["orderId"], 2991 self.ticker, self.figi, lots, 2992 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2993 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2994 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2995 )) 2996 2997 if tp > 0: 2998 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2999 3000 if sl > 0: 3001 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3002 3003 else: 3004 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 3005 3006 return response 3007 3008 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3009 """ 3010 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3011 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3012 3013 See also: `Order()` and `Trade()` docstrings. 3014 3015 :param lots: volume, integer count of lots >= 1. 3016 :param tp: float > 0, take profit price of stop-order. 3017 :param sl: float > 0, stop loss price of stop-order. 3018 :param expDate: it's a local date in future. 3019 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3020 :return: JSON with response from broker server. 3021 """ 3022 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3023 3024 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3025 """ 3026 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3027 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3028 3029 See also: `Order()` and `Trade()` docstrings. 3030 3031 :param lots: volume, integer count of lots >= 1. 3032 :param tp: float > 0, take profit price of stop-order. 3033 :param sl: float > 0, stop loss price of stop-order. 3034 :param expDate: it's a local date in the future. 3035 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3036 :return: JSON with response from broker server. 3037 """ 3038 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3039 3040 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3041 """ 3042 Close position of given instruments. 3043 3044 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3045 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3046 This avoids unnecessary downloading data from the server. 3047 """ 3048 if instruments is None or not instruments: 3049 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3050 raise Exception("Ticker or FIGI required") 3051 3052 if isinstance(instruments, str): 3053 instruments = [instruments] 3054 3055 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3056 if uniqueInstruments: 3057 if portfolio is None or not portfolio: 3058 portfolio = self.Overview(show=False) 3059 3060 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3061 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3062 3063 for self.figi in uniqueInstruments: 3064 if self.figi not in allOpened: 3065 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3066 continue 3067 3068 # search open trade info about instrument by ticker: 3069 instrument = {} 3070 for iType in TKS_INSTRUMENTS: 3071 if instrument: 3072 break 3073 3074 for item in portfolio["stat"][iType]: 3075 if item["figi"] == self.figi: 3076 instrument = item 3077 break 3078 3079 if instrument: 3080 self.ticker = instrument["ticker"] 3081 self.figi = instrument["figi"] 3082 3083 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3084 self.ticker, 3085 self.figi, 3086 int(instrument["volume"]), 3087 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3088 )) 3089 3090 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3091 3092 if tradeLots > 0: 3093 if instrument["blocked"] > 0: 3094 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3095 instrument["blocked"], 3096 self.ticker, 3097 tradeLots, 3098 )) 3099 3100 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3101 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3102 3103 else: 3104 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3105 3106 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3107 """ 3108 Close all positions of given instruments with defined type. 3109 3110 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3111 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3112 This avoids unnecessary downloading data from the server. 3113 """ 3114 if iType not in TKS_INSTRUMENTS: 3115 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3116 3117 else: 3118 if portfolio is None or not portfolio: 3119 portfolio = self.Overview(show=False) 3120 3121 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3122 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3123 3124 if tickers and portfolio: 3125 self.CloseTrades(tickers, portfolio) 3126 3127 else: 3128 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3129 3130 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3131 """ 3132 Universal method to create market or limit orders with all available parameters for current `accountId`. 3133 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3134 3135 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3136 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3137 3138 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3139 then broker immediately open market order as you can do simple --buy or --sell operations! 3140 3141 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3142 When current price will go up or down to target price value then broker opens a limit order. 3143 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3144 3145 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3146 3147 :param operation: string "Buy" or "Sell". 3148 :param orderType: string "Limit" or "Stop". 3149 :param lots: volume, integer count of lots >= 1. 3150 :param targetPrice: target price > 0. This is open trade price for limit order. 3151 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3152 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3153 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3154 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3155 Stop loss order always executed by market price. 3156 :param expDate: string "Undefined" by default or local date in future. 3157 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3158 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3159 A limit order has no expiration date, it lasts until the end of the trading day. 3160 :return: JSON with response from broker server. 3161 """ 3162 if self.accountId is None or not self.accountId: 3163 uLogger.error("Variable `accountId` must be defined for using this method!") 3164 raise Exception("Account ID required") 3165 3166 if operation is None or not operation or operation not in ("Buy", "Sell"): 3167 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3168 raise Exception("Incorrect value") 3169 3170 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3171 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3172 raise Exception("Incorrect value") 3173 3174 if lots is None or lots < 1: 3175 uLogger.error("You must define trade volume > 0: integer count of lots!") 3176 raise Exception("Incorrect value") 3177 3178 if targetPrice is None or targetPrice <= 0: 3179 uLogger.error("Target price for limit-order must be greater than 0!") 3180 raise Exception("Incorrect value") 3181 3182 if limitPrice is None or limitPrice <= 0: 3183 limitPrice = targetPrice 3184 3185 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3186 stopType = "Limit" 3187 3188 if expDate is None or not expDate: 3189 expDate = "Undefined" 3190 3191 if not (self.ticker or self.figi): 3192 uLogger.error("Tocker or FIGI must be defined!") 3193 raise Exception("Ticker or FIGI required") 3194 3195 response = {} 3196 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3197 self.ticker = instrument["ticker"] 3198 self.figi = instrument["figi"] 3199 3200 if orderType == "Limit": 3201 uLogger.debug( 3202 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3203 self.ticker, self.figi, 3204 operation, lots, targetPrice, instrument["currency"], 3205 )) 3206 3207 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3208 self.body = str({ 3209 "figi": self.figi, 3210 "quantity": str(lots), 3211 "price": FloatToNano(targetPrice), 3212 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3213 "accountId": str(self.accountId), 3214 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3215 }) 3216 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3217 3218 if "orderId" in response.keys(): 3219 uLogger.info( 3220 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3221 response["orderId"], 3222 self.ticker, self.figi, 3223 operation, lots, targetPrice, instrument["currency"], 3224 )) 3225 3226 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3227 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3228 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3229 targetPrice, instrument["currency"], 3230 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3231 )) 3232 3233 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3234 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3235 targetPrice, instrument["currency"], 3236 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3237 )) 3238 3239 else: 3240 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3241 3242 if orderType == "Stop": 3243 uLogger.debug( 3244 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3245 self.ticker, self.figi, 3246 operation, lots, 3247 targetPrice, instrument["currency"], 3248 limitPrice, instrument["currency"], 3249 stopType, expDate, 3250 )) 3251 3252 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3253 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3254 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3255 3256 body = { 3257 "figi": self.figi, 3258 "quantity": str(lots), 3259 "price": FloatToNano(limitPrice), 3260 "stopPrice": FloatToNano(targetPrice), 3261 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3262 "accountId": str(self.accountId), 3263 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3264 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3265 } 3266 3267 if expDateUTC: 3268 body["expireDate"] = expDateUTC 3269 3270 self.body = str(body) 3271 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3272 3273 if "stopOrderId" in response.keys(): 3274 uLogger.info( 3275 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3276 response["stopOrderId"], 3277 self.ticker, self.figi, 3278 operation, lots, 3279 targetPrice, instrument["currency"], 3280 limitPrice, instrument["currency"], 3281 TKS_STOP_ORDER_TYPES[stopOrderType], 3282 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3283 )) 3284 3285 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3286 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3287 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3288 targetPrice, instrument["currency"], 3289 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3290 )) 3291 3292 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3293 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3294 targetPrice, instrument["currency"], 3295 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3296 )) 3297 3298 else: 3299 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3300 3301 return response 3302 3303 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3304 """ 3305 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3306 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3307 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3308 See also: `Order()` docstring. 3309 3310 :param lots: volume, integer count of lots >= 1. 3311 :param targetPrice: target price > 0. This is open trade price for limit order. 3312 :return: JSON with response from broker server. 3313 """ 3314 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3315 3316 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3317 """ 3318 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3319 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3320 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3321 target price value then broker opens a limit order. See also: `Order()` docstring. 3322 3323 :param lots: volume, integer count of lots >= 1. 3324 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3325 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3326 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3327 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3328 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3329 :param expDate: string "Undefined" by default or local date in future. 3330 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3331 This date is converting to UTC format for server. 3332 :return: JSON with response from broker server. 3333 """ 3334 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3335 3336 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3337 """ 3338 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3339 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3340 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3341 See also: `Order()` docstring. 3342 3343 :param lots: volume, integer count of lots >= 1. 3344 :param targetPrice: target price > 0. This is open trade price for limit order. 3345 :return: JSON with response from broker server. 3346 """ 3347 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3348 3349 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3350 """ 3351 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3352 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3353 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3354 target price value then broker opens a limit order. See also: `Order()` docstring. 3355 3356 :param lots: volume, integer count of lots >= 1. 3357 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3358 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3359 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3360 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3361 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3362 :param expDate: string "Undefined" by default or local date in future. 3363 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3364 This date is converting to UTC format for server. 3365 :return: JSON with response from broker server. 3366 """ 3367 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3368 3369 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3370 """ 3371 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3372 3373 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3374 :param allOrdersIDs: pre-received lists of all active pending orders. 3375 This avoids unnecessary downloading data from the server. 3376 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3377 """ 3378 if self.accountId is None or not self.accountId: 3379 uLogger.error("Variable `accountId` must be defined for using this method!") 3380 raise Exception("Account ID required") 3381 3382 if orderIDs: 3383 if allOrdersIDs is None or not allOrdersIDs: 3384 rawOrders = self.RequestPendingOrders() 3385 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3386 3387 if allStopOrdersIDs is None or not allStopOrdersIDs: 3388 rawStopOrders = self.RequestStopOrders() 3389 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3390 3391 for orderID in orderIDs: 3392 idInPendingOrders = orderID in allOrdersIDs 3393 idInStopOrders = orderID in allStopOrdersIDs 3394 3395 if not (idInPendingOrders or idInStopOrders): 3396 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3397 continue 3398 3399 else: 3400 if idInPendingOrders: 3401 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3402 3403 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3404 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3405 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3406 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3407 3408 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3409 if self.moreDebug: 3410 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3411 3412 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3413 3414 else: 3415 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3416 3417 elif idInStopOrders: 3418 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3419 3420 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3421 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3422 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3423 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3424 3425 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3426 if self.moreDebug: 3427 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3428 3429 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3430 3431 else: 3432 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3433 3434 else: 3435 continue 3436 3437 def CloseAllOrders(self) -> None: 3438 """ 3439 Gets a list of open pending and stop orders and cancel it all. 3440 """ 3441 rawOrders = self.RequestPendingOrders() 3442 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3443 lenOrders = len(allOrdersIDs) 3444 3445 rawStopOrders = self.RequestStopOrders() 3446 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3447 lenSOrders = len(allStopOrdersIDs) 3448 3449 if lenOrders > 0 or lenSOrders > 0: 3450 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3451 3452 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3453 3454 else: 3455 uLogger.info("Orders not found, nothing to cancel.") 3456 3457 def CloseAll(self, *args) -> None: 3458 """ 3459 Close all available (not blocked) opened trades and orders. 3460 3461 Also, you can select one or more keywords case-insensitive: 3462 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3463 3464 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3465 """ 3466 overview = self.Overview(show=False) # get all open trades info 3467 3468 if len(args) == 0: 3469 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3470 self.CloseAllOrders() # close all pending and stop orders 3471 3472 for iType in TKS_INSTRUMENTS: 3473 if iType != "Currencies": 3474 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3475 3476 else: 3477 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3478 lowerArgs = [x.lower() for x in args] 3479 3480 if "orders" in lowerArgs: 3481 self.CloseAllOrders() # close all pending and stop orders 3482 3483 for iType in TKS_INSTRUMENTS: 3484 if iType.lower() in lowerArgs and iType != "Currencies": 3485 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3486 3487 @staticmethod 3488 def ParseOrderParameters(operation, **inputParameters): 3489 """ 3490 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3491 3492 :param operation: string "Buy" or "Sell". 3493 :param inputParameters: this is dict of strings that looks like this 3494 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3495 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3496 "prices" key: one or more prices to open limit-orders 3497 Counts of values in lots and prices lists must be equals! 3498 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3499 """ 3500 # TODO: update order grid work with api v2 3501 pass 3502 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3503 # 3504 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3505 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3506 # raise Exception("Incorrect value") 3507 # 3508 # if "l" in inputParameters.keys(): 3509 # inputParameters["lots"] = inputParameters.pop("l") 3510 # 3511 # if "p" in inputParameters.keys(): 3512 # inputParameters["prices"] = inputParameters.pop("p") 3513 # 3514 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3515 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3516 # raise Exception("Incorrect value") 3517 # 3518 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3519 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3520 # 3521 # if len(lots) != len(prices): 3522 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3523 # raise Exception("Incorrect value") 3524 # 3525 # uLogger.debug("Extracted parameters for orders:") 3526 # uLogger.debug("lots = {}".format(lots)) 3527 # uLogger.debug("prices = {}".format(prices)) 3528 # 3529 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3530 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3531 # uLogger.debug("Order parameters: {}".format(result)) 3532 # 3533 # return result 3534 3535 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3536 """ 3537 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3538 3539 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3540 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3541 """ 3542 result = False 3543 msg = "Instrument not defined!" 3544 3545 if portfolio is None or not portfolio: 3546 portfolio = self.Overview(show=False) 3547 3548 if self.ticker: 3549 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3550 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3551 3552 for iType in TKS_INSTRUMENTS: 3553 for instrument in portfolio["stat"][iType]: 3554 if instrument["ticker"] == self.ticker: 3555 result = True 3556 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3557 break 3558 3559 elif self.figi: 3560 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3561 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3562 3563 for iType in TKS_INSTRUMENTS: 3564 for instrument in portfolio["stat"][iType]: 3565 if instrument["figi"] == self.figi: 3566 result = True 3567 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3568 break 3569 3570 else: 3571 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3572 3573 uLogger.debug(msg) 3574 3575 return result 3576 3577 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3578 """ 3579 Returns instrument from the user's portfolio if it presents there. 3580 Instrument must be defined by `ticker` (highly priority) or `figi`. 3581 3582 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3583 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3584 """ 3585 result = None 3586 msg = "Instrument not defined!" 3587 3588 if portfolio is None or not portfolio: 3589 portfolio = self.Overview(show=False) 3590 3591 if self.ticker: 3592 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3593 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3594 3595 for iType in TKS_INSTRUMENTS: 3596 for instrument in portfolio["stat"][iType]: 3597 if instrument["ticker"] == self.ticker: 3598 result = instrument 3599 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3600 break 3601 3602 elif self.figi: 3603 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3604 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3605 3606 for iType in TKS_INSTRUMENTS: 3607 for instrument in portfolio["stat"][iType]: 3608 if instrument["figi"] == self.figi: 3609 result = instrument 3610 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3611 break 3612 3613 else: 3614 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3615 3616 uLogger.debug(msg) 3617 3618 return result 3619 3620 def RequestLimits(self) -> dict: 3621 """ 3622 Method for obtaining the available funds for withdrawal for current `accountId`. 3623 3624 See also: 3625 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3626 - `OverviewLimits()` method 3627 3628 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3629 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3630 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3631 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3632 """ 3633 if self.accountId is None or not self.accountId: 3634 uLogger.error("Variable `accountId` must be defined for using this method!") 3635 raise Exception("Account ID required") 3636 3637 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3638 3639 self.body = str({"accountId": self.accountId}) 3640 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3641 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3642 3643 if self.moreDebug: 3644 uLogger.debug("Records about available funds for withdrawal successfully received") 3645 3646 return rawLimits 3647 3648 def OverviewLimits(self, show: bool = False) -> dict: 3649 """ 3650 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3651 3652 See also: `RequestLimits()`. 3653 3654 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3655 :return: dict with raw parsed data from server and some calculated statistics about it. 3656 """ 3657 if self.accountId is None or not self.accountId: 3658 uLogger.error("Variable `accountId` must be defined for using this method!") 3659 raise Exception("Account ID required") 3660 3661 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3662 3663 view = { 3664 "rawLimits": rawLimits, 3665 "limits": { # parsed data for every currency: 3666 "money": { # this is an array of portfolio currency positions 3667 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3668 }, 3669 "blocked": { # this is an array of blocked currency 3670 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3671 }, 3672 "blockedGuarantee": { # this is locked money under collateral for futures 3673 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3674 }, 3675 }, 3676 } 3677 3678 # --- Prepare text table with limits in human-readable format: 3679 if show: 3680 info = [ 3681 "# Withdrawal limits\n\n", 3682 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3683 "* **Account ID:** [{}]\n".format(self.accountId), 3684 ] 3685 3686 if view["limits"]["money"]: 3687 info.extend([ 3688 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3689 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3690 ]) 3691 3692 else: 3693 info.append("\nNo withdrawal limits\n") 3694 3695 for curr in view["limits"]["money"].keys(): 3696 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3697 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3698 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3699 3700 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3701 "[{}]".format(curr), 3702 "{:.2f}".format(view["limits"]["money"][curr]), 3703 "{:.2f}".format(availableMoney), 3704 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3705 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3706 ) 3707 3708 if curr == "rub": 3709 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3710 3711 else: 3712 info.append(infoStr) 3713 3714 infoText = "".join(info) 3715 3716 uLogger.info(infoText) 3717 3718 if self.withdrawalLimitsFile: 3719 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3720 fH.write(infoText) 3721 3722 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3723 3724 return view 3725 3726 def RequestAccounts(self) -> dict: 3727 """ 3728 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3729 3730 See also: 3731 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3732 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3733 - `OverviewUserInfo()` method 3734 3735 :return: dict with raw data from server that contains accounts info. Example of dict: 3736 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3737 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3738 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3739 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3740 """ 3741 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3742 3743 self.body = str({}) 3744 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3745 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3746 3747 if self.moreDebug: 3748 uLogger.debug("Records about available accounts successfully received") 3749 3750 return rawAccounts 3751 3752 def RequestUserInfo(self) -> dict: 3753 """ 3754 Method for requesting common user's information. 3755 3756 See also: 3757 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3758 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3759 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3760 - `OverviewUserInfo()` method 3761 3762 :return: dict with raw data from server that contains user's information. Example of dict: 3763 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3764 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3765 """ 3766 uLogger.debug("Requesting common user's information. Wait, please...") 3767 3768 self.body = str({}) 3769 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3770 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3771 3772 if self.moreDebug: 3773 uLogger.debug("Records about current user successfully received") 3774 3775 return rawUserInfo 3776 3777 def RequestMarginStatus(self, accountId: str = None) -> dict: 3778 """ 3779 Method for requesting margin calculation for defined account ID. 3780 3781 See also: 3782 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3783 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3784 - `OverviewUserInfo()` method 3785 3786 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3787 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3788 Example of responses: 3789 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3790 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3791 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3792 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3793 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3794 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3795 """ 3796 if accountId is None or not accountId: 3797 if self.accountId is None or not self.accountId: 3798 uLogger.error("Variable `accountId` must be defined for using this method!") 3799 raise Exception("Account ID required") 3800 3801 else: 3802 accountId = self.accountId # use `self.accountId` (main ID) by default 3803 3804 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3805 3806 self.body = str({"accountId": accountId}) 3807 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3808 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3809 3810 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3811 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3812 rawMargin = {} 3813 3814 else: 3815 if self.moreDebug: 3816 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3817 3818 return rawMargin 3819 3820 def RequestTariffLimits(self) -> dict: 3821 """ 3822 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3823 3824 See also: 3825 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3826 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3827 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3828 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3829 - `OverviewUserInfo()` method 3830 3831 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3832 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3833 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3834 """ 3835 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3836 3837 self.body = str({}) 3838 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3839 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3840 3841 if self.moreDebug: 3842 uLogger.debug("Records with limits of current tariff successfully received") 3843 3844 return rawTariffLimits 3845 3846 def RequestBondCoupons(self, iJSON: dict) -> dict: 3847 """ 3848 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3849 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3850 All dates are in UTC timezone. 3851 3852 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3853 Documentation: 3854 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3855 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3856 3857 See also: `ExtendBondsData()`. 3858 3859 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3860 If raw iJSON is not data of bond then server returns an error [400] with message: 3861 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3862 :return: dictionary with bond payment calendar. Response example 3863 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3864 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3865 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3866 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3867 """ 3868 if iJSON["figi"] is None or not iJSON["figi"]: 3869 uLogger.error("FIGI must be defined for using this method!") 3870 raise Exception("FIGI required") 3871 3872 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3873 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3874 3875 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3876 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3877 self.figi, 3878 startDate, 3879 endDate, 3880 )) 3881 3882 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3883 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3884 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3885 3886 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3887 uLogger.warning("Instrument type is not bond!") 3888 3889 else: 3890 if self.moreDebug: 3891 uLogger.debug("Records about bond payment calendar successfully received") 3892 3893 return calendar 3894 3895 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3896 """ 3897 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3898 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3899 coupon yields, current yields and some statistics etc. 3900 3901 WARNING! This is too long operation if a lot of bonds requested from broker server. 3902 3903 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3904 3905 :param instruments: list of strings with tickers or FIGIs. 3906 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3907 for further used by data scientists or stock analytics. 3908 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3909 In XLSX-file and Pandas DataFrame fields mean: 3910 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3911 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3912 """ 3913 if instruments is None or not instruments: 3914 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3915 raise Exception("Ticker or FIGI required") 3916 3917 if isinstance(instruments, str): 3918 instruments = [instruments] 3919 3920 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3921 3922 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3923 3924 iCount = len(uniqueInstruments) 3925 tooLong = iCount >= 20 3926 if tooLong: 3927 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3928 3929 bonds = None 3930 for i, self.figi in enumerate(uniqueInstruments): 3931 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3932 3933 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3934 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3935 rawBond = self.SearchByFIGI(requestPrice=True) 3936 3937 # Widen raw data with UTC current time (iData["actualDateTime"]): 3938 actualDate = datetime.now(tzutc()) 3939 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3940 3941 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3942 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3943 3944 # Replace some values with human-readable: 3945 iData["nominalCurrency"] = iData["nominal"]["currency"] 3946 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3947 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3948 iData["aciCurrency"] = iData["aciValue"]["currency"] 3949 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3950 iData["issueSize"] = int(iData["issueSize"]) 3951 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3952 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3953 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3954 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3955 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3956 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3957 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3958 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3959 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3960 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3961 3962 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3963 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3964 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3965 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3966 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3967 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3968 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3969 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3970 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3971 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3972 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3973 3974 # Widen raw data with calendar data from `rawCalendar` values: 3975 calendarData = [] 3976 if "events" in iData["rawCalendar"].keys(): 3977 for item in iData["rawCalendar"]["events"]: 3978 calendarData.append({ 3979 "couponDate": item["couponDate"], 3980 "couponNumber": int(item["couponNumber"]), 3981 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3982 "payCurrency": item["payOneBond"]["currency"], 3983 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3984 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3985 "couponStartDate": item["couponStartDate"], 3986 "couponEndDate": item["couponEndDate"], 3987 "couponPeriod": item["couponPeriod"], 3988 }) 3989 3990 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3991 if "maturityDate" not in iData.keys(): 3992 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3993 3994 # Widen raw data with Coupon Rate. 3995 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3996 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3997 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3998 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3999 4000 # Widen raw data with Yield to Maturity (YTM) on current date. 4001 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4002 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4003 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4004 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4005 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4006 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4007 4008 iData["calendar"] = calendarData # adds calendar at the end 4009 4010 # Remove not used data: 4011 iData.pop("uid") 4012 iData.pop("positionUid") 4013 iData.pop("currentPrice") 4014 iData.pop("rawCalendar") 4015 4016 colNames = list(iData.keys()) 4017 if bonds is None: 4018 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4019 4020 else: 4021 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4022 4023 else: 4024 uLogger.warning("Instrument is not a bond!") 4025 4026 processed = round(100 * (i + 1) / iCount, 1) 4027 if tooLong and processed % 5 == 0: 4028 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4029 4030 else: 4031 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4032 4033 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4034 4035 # Saving bonds from Pandas DataFrame to XLSX sheet: 4036 if xlsx and self.bondsXLSXFile: 4037 with pd.ExcelWriter( 4038 path=self.bondsXLSXFile, 4039 date_format=TKS_DATE_FORMAT, 4040 datetime_format=TKS_DATE_TIME_FORMAT, 4041 mode="w", 4042 ) as writer: 4043 bonds.to_excel( 4044 writer, 4045 sheet_name="Extended bonds data", 4046 index=True, 4047 encoding="UTF-8", 4048 freeze_panes=(1, 1), 4049 ) # saving as XLSX-file with freeze first row and column as headers 4050 4051 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4052 4053 return bonds 4054 4055 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4056 """ 4057 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4058 4059 WARNING! This is too long operation if a lot of bonds requested from broker server. 4060 4061 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4062 4063 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4064 extended information about bonds: main info, current prices, bond payment calendar, 4065 coupon yields, current yields and some statistics etc. 4066 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4067 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4068 for further used by data scientists or stock analytics. 4069 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4070 """ 4071 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4072 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4073 4074 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4075 4076 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4077 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4078 calendar = None 4079 for bond in extBonds.iterrows(): 4080 for item in bond[1]["calendar"]: 4081 cData = { 4082 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4083 "couponDate": item["couponDate"], 4084 "figi": bond[1]["figi"], 4085 "ticker": bond[1]["ticker"], 4086 "name": bond[1]["name"], 4087 "couponNumber": item["couponNumber"], 4088 "payOneBond": item["payOneBond"], 4089 "payCurrency": item["payCurrency"], 4090 "couponType": item["couponType"], 4091 "couponPeriod": item["couponPeriod"], 4092 "fixDate": item["fixDate"], 4093 "couponStartDate": item["couponStartDate"], 4094 "couponEndDate": item["couponEndDate"], 4095 } 4096 4097 if calendar is None: 4098 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4099 4100 else: 4101 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4102 4103 if calendar is not None: 4104 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4105 4106 # Saving calendar from Pandas DataFrame to XLSX sheet: 4107 if xlsx: 4108 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4109 4110 with pd.ExcelWriter( 4111 path=xlsxCalendarFile, 4112 date_format=TKS_DATE_FORMAT, 4113 datetime_format=TKS_DATE_TIME_FORMAT, 4114 mode="w", 4115 ) as writer: 4116 humanReadable = calendar.copy(deep=True) 4117 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4118 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4119 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4120 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4121 humanReadable.columns = colNames # human-readable column names 4122 4123 humanReadable.to_excel( 4124 writer, 4125 sheet_name="Bond payments calendar", 4126 index=False, 4127 encoding="UTF-8", 4128 freeze_panes=(1, 2), 4129 ) # saving as XLSX-file with freeze first row and column as headers 4130 4131 del humanReadable # release df in memory 4132 4133 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4134 4135 return calendar 4136 4137 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4138 """ 4139 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4140 Also, creates Markdown file with calendar data, `calendar.md` by default. 4141 4142 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4143 4144 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4145 extended information about bonds: main info, current prices, bond payment calendar, 4146 coupon yields, current yields and some statistics etc. 4147 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4148 :param show: if `True` then also printing bonds payment calendar to the console, 4149 otherwise save to file `calendarFile` only. `False` by default. 4150 :return: multilines text in Markdown format with bonds payment calendar as a table. 4151 """ 4152 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4153 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4154 4155 infoText = "# Bond payments calendar\n\n" 4156 4157 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4158 4159 if not (calendar is None or calendar.empty): 4160 splitLine = "| | | | | | | | | |\n" 4161 4162 info = [ 4163 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4164 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4165 ] 4166 4167 newMonth = False 4168 notOneBond = calendar["figi"].nunique() > 1 4169 for i, bond in enumerate(calendar.iterrows()): 4170 if newMonth and notOneBond: 4171 info.append(splitLine) 4172 4173 info.append( 4174 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4175 " √" if bond[1]["paid"] else " —", 4176 bond[1]["couponDate"].split("T")[0], 4177 bond[1]["figi"], 4178 bond[1]["ticker"], 4179 bond[1]["couponNumber"], 4180 "{} {}".format( 4181 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4182 bond[1]["payCurrency"], 4183 ), 4184 bond[1]["couponType"], 4185 bond[1]["couponPeriod"], 4186 bond[1]["fixDate"].split("T")[0], 4187 ) 4188 ) 4189 4190 if i < len(calendar.values) - 1: 4191 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4192 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4193 newMonth = False if curDate.month == nextDate.month else True 4194 4195 else: 4196 newMonth = False 4197 4198 infoText += "".join(info) 4199 4200 if show: 4201 uLogger.info("{}".format(infoText)) 4202 4203 if self.calendarFile is not None: 4204 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4205 fH.write(infoText) 4206 4207 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4208 4209 else: 4210 infoText += "No data\n" 4211 4212 return infoText 4213 4214 def OverviewAccounts(self, show: bool = False) -> dict: 4215 """ 4216 Method for parsing and show simple table with all available user accounts. 4217 4218 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4219 4220 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4221 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4222 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4223 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4224 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4225 "closed": "—", "access": "Full access" }, ...}}` 4226 """ 4227 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4228 4229 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4230 accounts = { 4231 item["id"]: { 4232 "type": TKS_ACCOUNT_TYPES[item["type"]], 4233 "name": item["name"], 4234 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4235 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4236 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4237 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4238 } for item in rawAccounts["accounts"] 4239 } 4240 4241 # Raw and parsed data with some fields replaced in "stat" section: 4242 view = { 4243 "rawAccounts": rawAccounts, 4244 "stat": accounts, 4245 } 4246 4247 # --- Prepare simple text table with only accounts data in human-readable format: 4248 if show: 4249 info = [ 4250 "# User accounts\n\n", 4251 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4252 "| Account ID | Type | Status | Name |\n", 4253 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4254 ] 4255 4256 for account in view["stat"].keys(): 4257 info.extend([ 4258 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4259 account, 4260 view["stat"][account]["type"], 4261 view["stat"][account]["status"], 4262 view["stat"][account]["name"], 4263 ) 4264 ]) 4265 4266 infoText = "".join(info) 4267 4268 uLogger.info(infoText) 4269 4270 if self.userAccountsFile: 4271 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4272 fH.write(infoText) 4273 4274 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4275 4276 return view 4277 4278 def OverviewUserInfo(self, show: bool = False) -> dict: 4279 """ 4280 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4281 4282 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4283 4284 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4285 :return: dict with raw parsed data from server and some calculated statistics about it. 4286 """ 4287 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4288 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4289 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4290 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4291 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4292 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4293 4294 # This is dict with parsed common user data: 4295 userInfo = { 4296 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4297 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4298 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4299 "tariff": rawUserInfo["tariff"], 4300 } 4301 4302 # This is an array of dict with parsed margin statuses for every account IDs: 4303 margins = {} 4304 for accountId in accounts.keys(): 4305 if rawMargins[accountId]: 4306 margins[accountId] = { 4307 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4308 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4309 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4310 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4311 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4312 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4313 } 4314 4315 else: 4316 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4317 4318 unary = {} # unary-connection limits 4319 for item in rawTariffLimits["unaryLimits"]: 4320 if item["limitPerMinute"] in unary.keys(): 4321 unary[item["limitPerMinute"]].extend(item["methods"]) 4322 4323 else: 4324 unary[item["limitPerMinute"]] = item["methods"] 4325 4326 stream = {} # stream-connection limits 4327 for item in rawTariffLimits["streamLimits"]: 4328 if item["limit"] in stream.keys(): 4329 stream[item["limit"]].extend(item["streams"]) 4330 4331 else: 4332 stream[item["limit"]] = item["streams"] 4333 4334 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4335 limits = { 4336 "unary": unary, 4337 "stream": stream, 4338 } 4339 4340 # Raw and parsed data as an output result: 4341 view = { 4342 "rawUserInfo": rawUserInfo, 4343 "rawAccounts": rawAccounts, 4344 "rawMargins": rawMargins, 4345 "rawTariffLimits": rawTariffLimits, 4346 "stat": { 4347 "userInfo": userInfo, 4348 "accounts": accounts, 4349 "margins": margins, 4350 "limits": limits, 4351 }, 4352 } 4353 4354 # --- Prepare text table with user information in human-readable format: 4355 if show: 4356 info = [ 4357 "# Full user information\n\n", 4358 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4359 "## Common information\n\n", 4360 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4361 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4362 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4363 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4364 "\n## User accounts\n\n", 4365 ] 4366 4367 for account in view["stat"]["accounts"].keys(): 4368 info.extend([ 4369 "### ID: [{}]\n\n".format(account), 4370 "| Parameters | Values |\n", 4371 "|----------------------|--------------------------------------------------------------|\n", 4372 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4373 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4374 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4375 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4376 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4377 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4378 ]) 4379 4380 if margins[account]: 4381 info.extend([ 4382 "| Margin status: | Enabled |\n", 4383 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4384 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4385 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4386 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4387 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4388 ]) 4389 4390 else: 4391 info.append("| Margin status: | Disabled |\n\n") 4392 4393 info.extend([ 4394 "\n## Current user tariff limits\n", 4395 "\nSee also:\n", 4396 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4397 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4398 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4399 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4400 "\n### Unary limits\n", 4401 ]) 4402 4403 if unary: 4404 for key, values in sorted(unary.items()): 4405 info.append("\n* Max requests per minute: {}\n".format(key)) 4406 4407 for value in values: 4408 info.append(" - {}\n".format(value)) 4409 4410 else: 4411 info.append("\nNot available\n") 4412 4413 info.append("\n### Stream limits\n") 4414 4415 if stream: 4416 for key, values in sorted(stream.items()): 4417 info.append("\n* Max stream connections: {}\n".format(key)) 4418 4419 for value in values: 4420 info.append(" - {}\n".format(value)) 4421 4422 else: 4423 info.append("\nNot available\n") 4424 4425 infoText = "".join(info) 4426 4427 uLogger.info(infoText) 4428 4429 if self.userInfoFile: 4430 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4431 fH.write(infoText) 4432 4433 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4434 4435 return view 4436 4437 4438class Args: 4439 """ 4440 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4441 """ 4442 def __init__(self, **kwargs): 4443 self.__dict__.update(kwargs) 4444 4445 def __getattr__(self, item): 4446 return None 4447 4448 4449def ParseArgs(): 4450 """This function get and parse command line keys.""" 4451 parser = ArgumentParser() # command-line string parser 4452 4453 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4454 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4455 4456 # --- options: 4457 4458 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4459 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4460 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4461 4462 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4463 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4464 4465 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4466 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4467 4468 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4469 4470 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4471 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4472 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4473 4474 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4475 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4476 4477 # --- commands: 4478 4479 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4480 4481 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4482 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4483 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4484 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4485 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4486 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4487 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4488 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4489 4490 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4491 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4492 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4493 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4494 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4495 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4496 4497 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4498 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4499 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4500 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4501 4502 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4503 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4504 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4505 4506 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4507 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4508 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4509 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4510 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4511 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4512 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4513 4514 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4515 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4516 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4517 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4518 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4519 4520 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4521 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4522 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4523 4524 cmdArgs = parser.parse_args() 4525 return cmdArgs 4526 4527 4528def Main(**kwargs): 4529 """ 4530 Main function for work with TKSBrokerAPI in the console. 4531 4532 See examples: 4533 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4534 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4535 """ 4536 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4537 4538 if args.debug_level: 4539 uLogger.level = 10 # always debug level by default 4540 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4541 4542 exitCode = 0 4543 start = datetime.now(tzutc()) 4544 uLogger.debug("=-" * 50) 4545 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4546 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4547 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4548 )) 4549 4550 # trying to calculate full current version: 4551 buildVersion = __version__ 4552 try: 4553 v = version("tksbrokerapi") 4554 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4555 4556 except Exception: 4557 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4558 4559 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4560 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4561 4562 try: 4563 if args.version: 4564 print("TKSBrokerAPI {}".format(buildVersion)) 4565 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4566 4567 else: 4568 # Init class for trading with Tinkoff Broker: 4569 trader = TinkoffBrokerServer( 4570 token=args.token, 4571 accountId=args.account_id, 4572 useCache=not args.no_cache, 4573 ) 4574 4575 # --- set some options: 4576 4577 if args.more: 4578 trader.moreDebug = True 4579 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4580 4581 if args.ticker: 4582 ticker = args.ticker.upper() # Tickers may be upper case only 4583 4584 if ticker in trader.aliasesKeys: 4585 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4586 4587 else: 4588 trader.ticker = ticker 4589 4590 if args.figi: 4591 trader.figi = args.figi.upper() # FIGIs may be upper case only 4592 4593 if args.depth is not None: 4594 trader.depth = args.depth 4595 4596 # --- do one command: 4597 4598 if args.list: 4599 if args.output is not None: 4600 trader.instrumentsFile = args.output 4601 4602 trader.ShowInstrumentsInfo(show=True) 4603 4604 elif args.list_xlsx: 4605 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4606 4607 elif args.bonds_xlsx is not None: 4608 if args.output is not None: 4609 trader.bondsXLSXFile = args.output 4610 4611 if len(args.bonds_xlsx) == 0: 4612 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4613 4614 else: 4615 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4616 4617 elif args.search: 4618 if args.output is not None: 4619 trader.searchResultsFile = args.output 4620 4621 trader.SearchInstruments(pattern=args.search[0], show=True) 4622 4623 elif args.info: 4624 if not (args.ticker or args.figi): 4625 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4626 raise Exception("Ticker or FIGI required") 4627 4628 if args.output is not None: 4629 trader.infoFile = args.output 4630 4631 if args.ticker: 4632 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4633 4634 else: 4635 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4636 4637 elif args.calendar is not None: 4638 if args.output is not None: 4639 trader.calendarFile = args.output 4640 4641 if len(args.calendar) == 0: 4642 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4643 4644 else: 4645 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4646 4647 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4648 4649 elif args.price: 4650 if not (args.ticker or args.figi): 4651 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4652 raise Exception("Ticker or FIGI required") 4653 4654 trader.GetCurrentPrices(show=True) 4655 4656 elif args.prices is not None: 4657 if args.output is not None: 4658 trader.pricesFile = args.output 4659 4660 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4661 4662 elif args.overview: 4663 if args.output is not None: 4664 trader.overviewFile = args.output 4665 4666 trader.Overview(show=True, details="full") 4667 4668 elif args.overview_digest: 4669 if args.output is not None: 4670 trader.overviewDigestFile = args.output 4671 4672 trader.Overview(show=True, details="digest") 4673 4674 elif args.overview_positions: 4675 if args.output is not None: 4676 trader.overviewPositionsFile = args.output 4677 4678 trader.Overview(show=True, details="positions") 4679 4680 elif args.overview_orders: 4681 if args.output is not None: 4682 trader.overviewOrdersFile = args.output 4683 4684 trader.Overview(show=True, details="orders") 4685 4686 elif args.overview_analytics: 4687 if args.output is not None: 4688 trader.overviewAnalyticsFile = args.output 4689 4690 trader.Overview(show=True, details="analytics") 4691 4692 elif args.overview_calendar: 4693 if args.output is not None: 4694 trader.overviewAnalyticsFile = args.output 4695 4696 trader.Overview(show=True, details="calendar") 4697 4698 elif args.deals is not None: 4699 if args.output is not None: 4700 trader.reportFile = args.output 4701 4702 if 0 <= len(args.deals) < 3: 4703 trader.Deals( 4704 start=args.deals[0] if len(args.deals) >= 1 else None, 4705 end=args.deals[1] if len(args.deals) == 2 else None, 4706 show=True, # Always show deals report in console 4707 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4708 ) 4709 4710 else: 4711 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4712 raise Exception("Incorrect value") 4713 4714 elif args.history is not None: 4715 if args.output is not None: 4716 trader.historyFile = args.output 4717 4718 if 0 <= len(args.history) < 3: 4719 dataReceived = trader.History( 4720 start=args.history[0] if len(args.history) >= 1 else None, 4721 end=args.history[1] if len(args.history) == 2 else None, 4722 interval="hour" if args.interval is None or not args.interval else args.interval, 4723 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4724 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4725 show=True, # shows all downloaded candles in console 4726 ) 4727 4728 if args.render_chart is not None and dataReceived is not None: 4729 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4730 4731 trader.ShowHistoryChart( 4732 candles=dataReceived, 4733 interact=iChart, 4734 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4735 ) 4736 4737 else: 4738 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4739 raise Exception("Incorrect value") 4740 4741 elif args.load_history is not None: 4742 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4743 4744 if args.render_chart is not None and histData is not None: 4745 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4746 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4747 4748 trader.ShowHistoryChart( 4749 candles=histData, 4750 interact=iChart, 4751 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4752 ) 4753 4754 elif args.trade is not None: 4755 if 1 <= len(args.trade) <= 5: 4756 trader.Trade( 4757 operation=args.trade[0], 4758 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4759 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4760 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4761 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4762 ) 4763 4764 else: 4765 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4766 4767 elif args.buy is not None: 4768 if 0 <= len(args.buy) <= 4: 4769 trader.Buy( 4770 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4771 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4772 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4773 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4774 ) 4775 4776 else: 4777 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4778 4779 elif args.sell is not None: 4780 if 0 <= len(args.sell) <= 4: 4781 trader.Sell( 4782 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4783 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4784 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4785 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4786 ) 4787 4788 else: 4789 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4790 4791 elif args.order: 4792 if 4 <= len(args.order) <= 7: 4793 trader.Order( 4794 operation=args.order[0], 4795 orderType=args.order[1], 4796 lots=int(args.order[2]), 4797 targetPrice=float(args.order[3]), 4798 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4799 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4800 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4801 ) 4802 4803 else: 4804 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4805 4806 elif args.buy_limit: 4807 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4808 4809 elif args.sell_limit: 4810 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4811 4812 elif args.buy_stop: 4813 if 2 <= len(args.buy_stop) <= 7: 4814 trader.BuyStop( 4815 lots=int(args.buy_stop[0]), 4816 targetPrice=float(args.buy_stop[1]), 4817 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4818 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4819 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4820 ) 4821 4822 else: 4823 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4824 4825 elif args.sell_stop: 4826 if 2 <= len(args.sell_stop) <= 7: 4827 trader.SellStop( 4828 lots=int(args.sell_stop[0]), 4829 targetPrice=float(args.sell_stop[1]), 4830 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4831 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4832 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4833 ) 4834 4835 else: 4836 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4837 4838 # elif args.buy_order_grid is not None: 4839 # # update order grid work with api v2 4840 # if len(args.buy_order_grid) == 2: 4841 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4842 # 4843 # for order in orderParams: 4844 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4845 # 4846 # else: 4847 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4848 # 4849 # elif args.sell_order_grid is not None: 4850 # # update order grid work with api v2 4851 # if len(args.sell_order_grid) >= 2: 4852 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4853 # 4854 # for order in orderParams: 4855 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4856 # 4857 # else: 4858 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4859 4860 elif args.close_order is not None: 4861 trader.CloseOrders(args.close_order) # close only one order 4862 4863 elif args.close_orders is not None: 4864 trader.CloseOrders(args.close_orders) # close list of orders 4865 4866 elif args.close_trade: 4867 if not (args.ticker or args.figi): 4868 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4869 raise Exception("Ticker or FIGI required") 4870 4871 if args.ticker: 4872 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4873 4874 else: 4875 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4876 4877 elif args.close_trades is not None: 4878 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4879 4880 elif args.close_all is not None: 4881 trader.CloseAll(*args.close_all) 4882 4883 elif args.limits: 4884 if args.output is not None: 4885 trader.withdrawalLimitsFile = args.output 4886 4887 trader.OverviewLimits(show=True) 4888 4889 elif args.user_info: 4890 if args.output is not None: 4891 trader.userInfoFile = args.output 4892 4893 trader.OverviewUserInfo(show=True) 4894 4895 elif args.account: 4896 if args.output is not None: 4897 trader.userAccountsFile = args.output 4898 4899 trader.OverviewAccounts(show=True) 4900 4901 else: 4902 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4903 raise Exception("There is no command to execute") 4904 4905 except Exception: 4906 trace = tb.format_exc() 4907 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4908 if e in trace: 4909 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4910 break 4911 4912 uLogger.debug(trace) 4913 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4914 exitCode = 255 # an error occurred, must be open a ticket for this issue 4915 4916 finally: 4917 finish = datetime.now(tzutc()) 4918 4919 if exitCode == 0: 4920 if args.more: 4921 uLogger.debug("All operations were finished success (summary code is 0).") 4922 4923 else: 4924 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4925 os.path.abspath(uLog.defaultLogFile), exitCode, 4926 )) 4927 4928 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4929 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4930 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4931 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4932 )) 4933 uLogger.debug("=-" * 50) 4934 4935 if not kwargs: 4936 sys.exit(exitCode) 4937 4938 else: 4939 return exitCode 4940 4941 4942if __name__ == "__main__": 4943 Main()
80def NanoToFloat(units: str, nano: int) -> float: 81 """ 82 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 83 84 `NanoToFloat(units="2", nano=500000000) -> 2.5` 85 86 `NanoToFloat(units="0", nano=50000000) -> 0.05` 87 88 :param units: integer string or integer parameter that represents the integer part of number 89 :param nano: integer string or integer parameter that represents the fractional part of number 90 :return: float view of number 91 """ 92 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
95def FloatToNano(number: float) -> dict: 96 """ 97 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 98 99 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 100 101 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 102 103 :param number: float number 104 :return: nano-type view of number: `{"units": "string", "nano": integer}` 105 """ 106 splitByPoint = str(number).split(".") 107 frac = 0 108 109 if len(splitByPoint) > 1: 110 if len(splitByPoint[1]) <= 9: 111 frac = int("{}{}".format( 112 int(splitByPoint[1]), 113 "0" * (9 - len(splitByPoint[1])), 114 )) 115 116 if (number < 0) and (frac > 0): 117 frac = -frac 118 119 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
122def GetDatesAsString(start: str = None, end: str = None) -> tuple: 123 """ 124 Create tuple of date and time strings with timezone parsed from user-friendly date. 125 126 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 127 128 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 129 An error exception will occur if input date has incorrect format. 130 131 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 132 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 133 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 134 Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago. 135 136 Also, you can use keywords for start if `end=None`: 137 `today` (from 00:00:00 to the end of current day), 138 `yesterday` (-1 day from 00:00:00 to 23:59:59), 139 `week` (-7 day from 00:00:00 to the end of current day), 140 `month` (-30 day from 00:00:00 to the end of current day), 141 `year` (-365 day from 00:00:00 to the end of current day), 142 143 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 144 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 145 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 146 """ 147 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 148 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 149 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 150 151 # time between start and the end of the current day: 152 if start is None or start.lower() == "today": 153 pass 154 155 # from start of the last day to the end of the last day: 156 elif start.lower() == "yesterday": 157 s -= timedelta(days=1) 158 e -= timedelta(days=1) 159 160 # week (-7 day from 00:00:00 to the end of the current day): 161 elif start.lower() == "week": 162 s -= timedelta(days=6) # +1 current day already taken into account 163 164 # month (-30 day from 00:00:00 to the end of current day): 165 elif start.lower() == "month": 166 s -= timedelta(days=29) # +1 current day already taken into account 167 168 # year (-365 day from 00:00:00 to the end of current day): 169 elif start.lower() == "year": 170 s -= timedelta(days=364) # +1 current day already taken into account 171 172 # -N days ago to the end of current day: 173 elif start.startswith('-') and start[1:].isdigit(): 174 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 175 176 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 177 else: 178 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 179 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 180 181 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 182 s = s.strftime(TKS_DATE_TIME_FORMAT) 183 e = e.strftime(TKS_DATE_TIME_FORMAT) 184 185 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 186 187 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 — how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
190class TinkoffBrokerServer: 191 """ 192 This class implements methods to work with Tinkoff broker server. 193 194 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 195 196 About `token`: https://tinkoff.github.io/investAPI/token/ 197 """ 198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 253 254 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 255 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 256 257 See also: `SearchByTicker()`, `SearchInstruments()`. 258 """ 259 260 self.figi = "" 261 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 262 263 See also: `SearchByFIGI()`, `SearchInstruments()`. 264 """ 265 266 self.depth = 1 267 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 268 269 See also: `GetCurrentPrices()`. 270 """ 271 272 self.server = r"https://invest-public-api.tinkoff.ru/rest" 273 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 274 275 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 276 """ 277 278 uLogger.debug("Broker API server: {}".format(self.server)) 279 280 self.timeout = 15 281 """Server operations timeout in seconds. Default: `15`. 282 283 See also: `SendAPIRequest()`. 284 """ 285 286 self.headers = { 287 "Content-Type": "application/json", 288 "accept": "application/json", 289 "Authorization": "Bearer {}".format(self.token), 290 "x-app-name": "Tim55667757.TKSBrokerAPI", 291 } 292 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 293 294 See also: `SendAPIRequest()`. 295 """ 296 297 self.body = None 298 """Request body which send to broker server. Default: `None`. 299 300 See also: `SendAPIRequest()`. 301 """ 302 303 self.moreDebug = False 304 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 305 306 self.historyFile = None 307 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 308 309 See also: `History()`. 310 """ 311 312 self.htmlHistoryFile = "index.html" 313 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 314 315 See also: `ShowHistoryChart()`. 316 """ 317 318 self.instrumentsFile = "instruments.md" 319 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 320 321 See also: `ShowInstrumentsInfo()`. 322 """ 323 324 self.searchResultsFile = "search-results.md" 325 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 326 327 See also: `SearchInstruments()`. 328 """ 329 330 self.pricesFile = "prices.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `GetListOfPrices()`. 334 """ 335 336 self.infoFile = "info.md" 337 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 338 339 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 340 """ 341 342 self.bondsXLSXFile = "ext-bonds.xlsx" 343 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 344 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 345 346 See also: `ExtendBondsData()`. 347 """ 348 349 self.calendarFile = "calendar.md" 350 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 351 352 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 353 354 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 355 """ 356 357 self.overviewFile = "overview.md" 358 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 359 360 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 361 """ 362 363 self.overviewDigestFile = "overview-digest.md" 364 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 365 366 See also: `Overview()` with parameter `details="digest"`. 367 """ 368 369 self.overviewPositionsFile = "overview-positions.md" 370 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 371 372 See also: `Overview()` with parameter `details="positions"`. 373 """ 374 375 self.overviewOrdersFile = "overview-orders.md" 376 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 377 378 See also: `Overview()` with parameter `details="orders"`. 379 """ 380 381 self.overviewAnalyticsFile = "overview-analytics.md" 382 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 383 384 See also: `Overview()` with parameter `details="analytics"`. 385 """ 386 387 self.overviewBondsCalendarFile = "overview-calendar.md" 388 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 389 390 See also: `Overview()` with parameter `details="calendar"`. 391 """ 392 393 self.reportFile = "deals.md" 394 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 395 396 See also: `Deals()`. 397 """ 398 399 self.withdrawalLimitsFile = "limits.md" 400 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 401 402 See also: `OverviewLimits()` and `RequestLimits()`. 403 """ 404 405 self.userInfoFile = "user-info.md" 406 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 407 408 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 409 """ 410 411 self.userAccountsFile = "accounts.md" 412 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 413 414 See also: `OverviewAccounts()`, `RequestAccounts()`. 415 """ 416 417 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 418 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 419 420 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 421 422 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 423 """ 424 425 self.iList = None # init iList for raw instruments data 426 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 427 428 See also: `Listing()`, `DumpInstruments()`. 429 """ 430 431 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 432 if useCache: 433 if os.path.exists(self.iListDumpFile): 434 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 435 curTime = datetime.now(tzutc()) 436 437 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 438 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 439 440 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 441 442 else: 443 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 444 445 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 446 os.path.abspath(self.iListDumpFile), 447 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 448 )) 449 450 else: 451 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 452 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 453 454 else: 455 self.iList = self.Listing() # request new raw instruments data from broker server 456 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 457 458 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 459 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 460 461 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 462 """ 463 464 def _ParseJSON(self, rawData="{}") -> dict: 465 """ 466 Parse JSON from response string. 467 468 :param rawData: this is a string with JSON-formatted text. 469 :return: JSON (dictionary), parsed from server response string. 470 """ 471 responseJSON = json.loads(rawData) if rawData else {} 472 473 if self.moreDebug: 474 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 475 476 return responseJSON 477 478 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 479 """ 480 Send GET or POST request to broker server and receive JSON object. 481 482 self.header: must be defining with dictionary of headers. 483 self.body: if define then used as request body. None by default. 484 self.timeout: global request timeout, 15 seconds by default. 485 :param url: url with REST request. 486 :param reqType: send "GET" or "POST" request. "GET" by default. 487 :param retry: how many times retry after first request if an 5xx server errors occurred. 488 :param pause: sleep time in seconds between retries. 489 :return: response JSON (dictionary) from broker. 490 """ 491 if reqType not in ("GET", "POST"): 492 uLogger.error("You can define request type: 'GET' or 'POST'!") 493 raise Exception("Incorrect value") 494 495 if self.moreDebug: 496 uLogger.debug("Request parameters:") 497 uLogger.debug(" - REST API URL: {}".format(url)) 498 uLogger.debug(" - request type: {}".format(reqType)) 499 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 500 uLogger.debug(" - body:\n{}".format(self.body)) 501 502 # fast hack to avoid all operations with some tickers/FIGI 503 responseJSON = {} 504 oK = True 505 for item in self.exclude: 506 if item in url: 507 if self.moreDebug: 508 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 509 510 oK = False 511 break 512 513 if oK: 514 counter = 0 515 response = None 516 errMsg = "" 517 518 while not response and counter <= retry: 519 if reqType == "GET": 520 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 521 522 if reqType == "POST": 523 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 524 525 if self.moreDebug: 526 uLogger.debug("Response:") 527 uLogger.debug(" - status code: {}".format(response.status_code)) 528 uLogger.debug(" - reason: {}".format(response.reason)) 529 uLogger.debug(" - body length: {}".format(len(response.text))) 530 uLogger.debug(" - headers:\n{}".format(response.headers)) 531 532 # Server returns some headers: 533 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 534 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 535 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 536 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 537 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 538 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 539 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 540 sleep(rateLimitWait) 541 542 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 543 if 400 <= response.status_code < 500: 544 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 546 counter = retry + 1 547 548 if 500 <= response.status_code < 600: 549 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 550 uLogger.debug(" - not oK, {}".format(errMsg)) 551 counter += 1 552 553 if counter <= retry: 554 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 555 sleep(pause) 556 557 responseJSON = self._ParseJSON(rawData=response.text) 558 559 if errMsg: 560 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 561 uLogger.error(" - not oK, {}".format(errMsg)) 562 563 return responseJSON 564 565 def _IUpdater(self, iType: str) -> tuple: 566 """ 567 Request instrument by type from server. See available API methods for instruments: 568 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 569 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 570 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 571 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 572 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 573 574 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 575 :return: tuple with iType name and list of available instruments of current type for defined user token. 576 """ 577 result = [] 578 579 if iType in TKS_INSTRUMENTS: 580 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 581 582 # all instruments have the same body in API v2 requests: 583 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 584 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 585 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 586 587 return iType, result 588 589 def _IWrapper(self, kwargs): 590 """ 591 Wrapper runs instrument's update method `_IUpdater()`. 592 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 593 """ 594 return self._IUpdater(**kwargs) 595 596 def Listing(self) -> dict: 597 """ 598 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 599 600 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 601 """ 602 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 603 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 604 605 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 606 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 607 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 608 609 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 610 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 611 poolUpdater.close() 612 613 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 614 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 615 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 616 617 # calculate minimum price increment (step) for all instruments and set up instrument's type: 618 for iType in iList.keys(): 619 for ticker in iList[iType]: 620 iList[iType][ticker]["type"] = iType 621 622 if "minPriceIncrement" in iList[iType][ticker].keys(): 623 iList[iType][ticker]["step"] = NanoToFloat( 624 iList[iType][ticker]["minPriceIncrement"]["units"], 625 iList[iType][ticker]["minPriceIncrement"]["nano"], 626 ) 627 628 else: 629 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 630 631 return iList 632 633 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 634 """ 635 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 636 637 See also: `DumpInstruments()`, `Listing()`. 638 639 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 640 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 641 """ 642 if self.iListDumpFile is None or not self.iListDumpFile: 643 uLogger.error("Output name of dump file must be defined!") 644 raise Exception("Filename required") 645 646 if not self.iList or forceUpdate: 647 self.iList = self.Listing() 648 649 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 650 651 # Save as XLSX with separated sheets for every type of instruments: 652 with pd.ExcelWriter( 653 path=xlsxDumpFile, 654 date_format=TKS_DATE_FORMAT, 655 datetime_format=TKS_DATE_TIME_FORMAT, 656 mode="w", 657 ) as writer: 658 for iType in TKS_INSTRUMENTS: 659 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 660 df = df[sorted(df)] # sorted by column names 661 df = df.applymap( 662 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 663 na_action="ignore", 664 ) # converting numbers from nano-type to float in every cell 665 df.to_excel( 666 writer, 667 sheet_name=iType, 668 encoding="UTF-8", 669 freeze_panes=(1, 1), 670 ) # saving as XLSX-file with freeze first row and column as headers 671 672 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 673 674 def DumpInstruments(self, forceUpdate: bool = True) -> str: 675 """ 676 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 677 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 678 679 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 680 681 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 682 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 683 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 684 """ 685 if self.iListDumpFile is None or not self.iListDumpFile: 686 uLogger.error("Output name of dump file must be defined!") 687 raise Exception("Filename required") 688 689 if not self.iList or forceUpdate: 690 self.iList = self.Listing() 691 692 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 693 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 694 fH.write(jsonDump) 695 696 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 697 698 return jsonDump 699 700 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 701 """ 702 Show information about one instrument defined by json data and prints it in Markdown format. 703 704 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 705 706 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 707 :param show: if `True` then also printing information about instrument and its current price. 708 :return: multilines text in Markdown format with information about one instrument. 709 """ 710 splitLine = "| | |\n" 711 infoText = "" 712 713 if iJSON is not None and iJSON and isinstance(iJSON, dict): 714 info = [ 715 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 716 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 717 "| Parameters | Values |\n", 718 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 719 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 720 "| Full name: | {:<54} |\n".format(iJSON["name"]), 721 ] 722 723 if "sector" in iJSON.keys() and iJSON["sector"]: 724 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 725 726 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 727 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 728 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 729 ))) 730 731 info.extend([ 732 splitLine, 733 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 734 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 735 ]) 736 737 if "isin" in iJSON.keys() and iJSON["isin"]: 738 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 739 740 if "classCode" in iJSON.keys(): 741 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 742 743 info.extend([ 744 splitLine, 745 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 746 splitLine, 747 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 748 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 749 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 750 ]) 751 752 if iJSON["figi"]: 753 self.figi = iJSON["figi"] 754 iJSON = iJSON | self.RequestTradingStatus() 755 756 info.extend([ 757 splitLine, 758 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 759 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 760 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 761 ]) 762 763 info.append(splitLine) 764 765 if "type" in iJSON.keys() and iJSON["type"]: 766 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 767 768 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 769 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 770 771 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 772 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 773 774 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 775 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 776 777 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 778 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 779 780 if "focusType" in iJSON.keys() and iJSON["focusType"]: 781 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 782 783 if "assetType" in iJSON.keys() and iJSON["assetType"]: 784 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 785 786 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 787 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 788 789 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 790 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 791 792 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 793 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 794 795 if "currency" in iJSON.keys(): 796 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 797 798 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 799 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 800 801 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 802 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 803 804 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 805 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 806 807 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 808 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 809 810 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 811 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 812 813 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 814 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 815 816 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 817 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 818 819 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 820 info.append("| Perpetual bond: | Yes |\n") 821 822 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 823 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 824 825 iExt = None 826 if iJSON["type"] == "Bonds": 827 info.extend([ 828 splitLine, 829 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 830 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 831 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 832 iJSON["nominal"]["currency"], 833 )), 834 ]) 835 836 if "floatingCouponFlag" in iJSON.keys(): 837 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 838 839 if "amortizationFlag" in iJSON.keys(): 840 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 841 842 info.append(splitLine) 843 844 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 845 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 846 847 if iJSON["figi"]: 848 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 849 850 info.extend([ 851 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 852 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 853 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 854 ]) 855 856 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 857 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 858 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 859 iJSON["aciValue"]["currency"] 860 ))) 861 862 if "currentPrice" in iJSON.keys(): 863 info.append(splitLine) 864 865 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 866 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 867 868 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 869 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 870 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 871 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 872 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 873 874 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 875 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 876 877 info.extend([ 878 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 879 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 880 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 881 )), 882 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 883 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 884 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 885 )), 886 "| Changes between last deal price and last close | {:<54} |\n".format( 887 "{:.2f}%{}".format( 888 iJSON["currentPrice"]["changes"], 889 " ({}{:.2f} {})".format( 890 "+" if bondChangesDelta > 0 else "", 891 bondChangesDelta, 892 aciCurrency 893 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 894 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 895 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 896 currency 897 ), 898 ) 899 ), 900 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 901 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 902 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 903 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 904 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 905 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 906 )), 907 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 908 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 909 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 910 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 911 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 912 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 913 )), 914 ]) 915 916 if "lot" in iJSON.keys(): 917 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 918 919 if "step" in iJSON.keys() and iJSON["step"] != 0: 920 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 921 922 # Add bond payment calendar: 923 if iJSON["type"] == "Bonds": 924 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 925 info.extend(["\n", strCalendar]) 926 927 infoText += "".join(info) 928 929 if show: 930 uLogger.info("{}".format(infoText)) 931 932 else: 933 uLogger.debug("{}".format(infoText)) 934 935 if self.infoFile is not None: 936 with open(self.infoFile, "w", encoding="UTF-8") as fH: 937 fH.write(infoText) 938 939 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 940 941 return infoText 942 943 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 944 """ 945 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 946 947 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 948 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 949 :return: JSON formatted data with information about instrument. 950 """ 951 tickerJSON = {} 952 if self.moreDebug: 953 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 954 955 if not self.ticker: 956 uLogger.warning("self.ticker variable is not be empty!") 957 958 else: 959 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 960 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 961 raise Exception("Instrument not allowed") 962 963 if not self.iList: 964 self.iList = self.Listing() 965 966 if self.ticker in self.iList["Shares"].keys(): 967 tickerJSON = self.iList["Shares"][self.ticker] 968 if self.moreDebug: 969 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 970 971 elif self.ticker in self.iList["Currencies"].keys(): 972 tickerJSON = self.iList["Currencies"][self.ticker] 973 if self.moreDebug: 974 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 975 976 elif self.ticker in self.iList["Bonds"].keys(): 977 tickerJSON = self.iList["Bonds"][self.ticker] 978 if self.moreDebug: 979 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 980 981 elif self.ticker in self.iList["Etfs"].keys(): 982 tickerJSON = self.iList["Etfs"][self.ticker] 983 if self.moreDebug: 984 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 985 986 elif self.ticker in self.iList["Futures"].keys(): 987 tickerJSON = self.iList["Futures"][self.ticker] 988 if self.moreDebug: 989 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 990 991 if tickerJSON: 992 self.figi = tickerJSON["figi"] 993 994 if requestPrice: 995 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 996 997 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 998 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 999 1000 else: 1001 tickerJSON["currentPrice"]["changes"] = 0 1002 1003 if show: 1004 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1005 1006 else: 1007 if show: 1008 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1009 1010 return tickerJSON 1011 1012 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1013 """ 1014 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1015 1016 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1017 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1018 :return: JSON formatted data with information about instrument. 1019 """ 1020 figiJSON = {} 1021 if self.moreDebug: 1022 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1023 1024 if not self.figi: 1025 uLogger.warning("self.figi variable is not be empty!") 1026 1027 else: 1028 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1029 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1030 raise Exception("Instrument not allowed") 1031 1032 if not self.iList: 1033 self.iList = self.Listing() 1034 1035 for item in self.iList["Shares"].keys(): 1036 if self.figi == self.iList["Shares"][item]["figi"]: 1037 figiJSON = self.iList["Shares"][item] 1038 1039 if self.moreDebug: 1040 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1041 1042 break 1043 1044 if not figiJSON: 1045 for item in self.iList["Currencies"].keys(): 1046 if self.figi == self.iList["Currencies"][item]["figi"]: 1047 figiJSON = self.iList["Currencies"][item] 1048 1049 if self.moreDebug: 1050 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1051 1052 break 1053 1054 if not figiJSON: 1055 for item in self.iList["Bonds"].keys(): 1056 if self.figi == self.iList["Bonds"][item]["figi"]: 1057 figiJSON = self.iList["Bonds"][item] 1058 1059 if self.moreDebug: 1060 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1061 1062 break 1063 1064 if not figiJSON: 1065 for item in self.iList["Etfs"].keys(): 1066 if self.figi == self.iList["Etfs"][item]["figi"]: 1067 figiJSON = self.iList["Etfs"][item] 1068 1069 if self.moreDebug: 1070 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1071 1072 break 1073 1074 if not figiJSON: 1075 for item in self.iList["Futures"].keys(): 1076 if self.figi == self.iList["Futures"][item]["figi"]: 1077 figiJSON = self.iList["Futures"][item] 1078 1079 if self.moreDebug: 1080 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1081 1082 break 1083 1084 if figiJSON: 1085 self.figi = figiJSON["figi"] 1086 self.ticker = figiJSON["ticker"] 1087 1088 if requestPrice: 1089 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1090 1091 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1092 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1093 1094 else: 1095 figiJSON["currentPrice"]["changes"] = 0 1096 1097 if show: 1098 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1099 1100 else: 1101 if show: 1102 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1103 1104 return figiJSON 1105 1106 def GetCurrentPrices(self, show: bool = True) -> dict: 1107 """ 1108 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1109 `{"buy": [{"price": 1243.8, "quantity": 193}, 1110 {"price": 1244.0, "quantity": 168}, 1111 {"price": 1244.8, "quantity": 5}, 1112 {"price": 1245.0, "quantity": 61}, 1113 {"price": 1245.4, "quantity": 60}], 1114 "sell": [{"price": 1243.6, "quantity": 8}, 1115 {"price": 1242.6, "quantity": 10}, 1116 {"price": 1242.4, "quantity": 18}, 1117 {"price": 1242.2, "quantity": 50}, 1118 {"price": 1242.0, "quantity": 113}], 1119 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1120 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1121 - sell: list of dicts with Buyers prices, 1122 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1123 - quantity: volume value by current price in lots, 1124 - limitUp: current trade session limit price, maximum, 1125 - limitDown: current trade session limit price, minimum, 1126 - lastPrice: last deal price of the instrument, 1127 - closePrice: previous trade session close price of the instrument. 1128 1129 See also: `SearchByTicker()` and `SearchByFIGI()`. 1130 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1131 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1132 1133 :param show: if `True` then print DOM to log and console. 1134 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1135 If an error occurred then returns an empty record: 1136 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1137 """ 1138 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1139 1140 if self.depth < 1: 1141 uLogger.error("Depth of Market (DOM) must be >=1!") 1142 raise Exception("Incorrect value") 1143 1144 if not (self.ticker or self.figi): 1145 uLogger.error("self.ticker or self.figi variables must be defined!") 1146 raise Exception("Ticker or FIGI required") 1147 1148 if self.ticker and not self.figi: 1149 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1150 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1151 1152 if not self.ticker and self.figi: 1153 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1154 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1155 1156 if not self.figi: 1157 uLogger.error("FIGI is not defined!") 1158 raise Exception("Ticker or FIGI required") 1159 1160 else: 1161 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1162 1163 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1164 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1165 self.body = str({"figi": self.figi, "depth": self.depth}) 1166 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1167 1168 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1169 # list of dicts with sellers orders: 1170 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1171 1172 # list of dicts with buyers orders: 1173 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1174 1175 # max price of instrument at this time: 1176 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1177 1178 # min price of instrument at this time: 1179 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1180 1181 # last price of deal with instrument: 1182 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1183 1184 # last close price of instrument: 1185 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1186 1187 else: 1188 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1189 uLogger.debug("Server response: {}".format(pricesResponse)) 1190 1191 if show: 1192 if prices["buy"] or prices["sell"]: 1193 info = [ 1194 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1195 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1196 self.ticker, 1197 self.figi, 1198 self.depth, 1199 ), 1200 "-" * 60, "\n", 1201 " Orders of Buyers | Orders of Sellers\n", 1202 "-" * 60, "\n", 1203 " Sell prices (volumes) | Buy prices (volumes)\n", 1204 "-" * 60, "\n", 1205 ] 1206 1207 if not prices["buy"]: 1208 info.append(" | No orders!\n") 1209 sumBuy = 0 1210 1211 else: 1212 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1213 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1214 for item in maxMinSorted: 1215 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1216 1217 if not prices["sell"]: 1218 info.append("No orders! |\n") 1219 sumSell = 0 1220 1221 else: 1222 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1223 for item in prices["sell"]: 1224 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1225 1226 info.extend([ 1227 "-" * 60, "\n", 1228 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1229 "-" * 60, "\n", 1230 ]) 1231 1232 infoText = "".join(info) 1233 1234 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1235 1236 else: 1237 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1238 1239 return prices 1240 1241 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1242 """ 1243 This method get and show information about all available broker instruments for current user account. 1244 If `instrumentsFile` string is not empty then also save information to this file. 1245 1246 :param show: if `True` then print results to console, if `False` — print only to file. 1247 :return: multi-lines string with all available broker instruments 1248 """ 1249 if not self.iList: 1250 self.iList = self.Listing() 1251 1252 info = [ 1253 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1254 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1255 ] 1256 1257 # add instruments count by type: 1258 for iType in self.iList.keys(): 1259 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1260 1261 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1262 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1263 1264 # generating info tables with all instruments by type: 1265 for iType in self.iList.keys(): 1266 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1267 1268 for instrument in self.iList[iType].keys(): 1269 iName = self.iList[iType][instrument]["name"] # instrument's name 1270 if len(iName) > 57: 1271 iName = "{}...".format(iName[:54]) # right trim for a long string 1272 1273 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1274 self.iList[iType][instrument]["ticker"], 1275 iName, 1276 self.iList[iType][instrument]["figi"], 1277 self.iList[iType][instrument]["currency"], 1278 self.iList[iType][instrument]["lot"], 1279 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1280 )) 1281 1282 infoText = "".join(info) 1283 1284 if show: 1285 uLogger.info(infoText) 1286 1287 if self.instrumentsFile: 1288 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1289 fH.write(infoText) 1290 1291 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1292 1293 return infoText 1294 1295 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1296 """ 1297 This method search and show information about instruments by part of its ticker, FIGI or name. 1298 If `searchResultsFile` string is not empty then also save information to this file. 1299 1300 :param pattern: string with part of ticker, FIGI or instrument's name. 1301 :param show: if `True` then print results to console, if `False` — return list of result only. 1302 :return: list of dictionaries with all found instruments. 1303 """ 1304 if not self.iList: 1305 self.iList = self.Listing() 1306 1307 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1308 compiledPattern = re.compile(pattern, re.IGNORECASE) 1309 1310 for iType in self.iList: 1311 for instrument in self.iList[iType].values(): 1312 searchResult = compiledPattern.search(" ".join( 1313 [instrument["ticker"], instrument["figi"], instrument["name"]] 1314 )) 1315 1316 if searchResult: 1317 searchResults[iType][instrument["ticker"]] = instrument 1318 1319 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1320 info = [ 1321 "# Search results\n\n", 1322 "* **Search pattern:** [{}]\n".format(pattern), 1323 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1324 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1325 ] 1326 infoShort = info[:] 1327 1328 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1329 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1330 skippedLine = "| ... | ... | ... | ... |\n" 1331 1332 if resultsLen == 0: 1333 info.append("\nNo results\n") 1334 infoShort.append("\nNo results\n") 1335 uLogger.warning("No results. Try changing your search pattern.") 1336 1337 else: 1338 for iType in searchResults: 1339 iTypeValuesCount = len(searchResults[iType].values()) 1340 if iTypeValuesCount > 0: 1341 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1342 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1343 1344 for instrument in searchResults[iType].values(): 1345 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1346 instrument["type"], 1347 instrument["ticker"], 1348 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1349 instrument["figi"], 1350 )) 1351 1352 if iTypeValuesCount <= 5: 1353 infoShort.extend(info[-iTypeValuesCount:]) 1354 1355 else: 1356 infoShort.extend(info[-5:]) 1357 infoShort.append(skippedLine) 1358 1359 infoText = "".join(info) 1360 infoTextShort = "".join(infoShort) 1361 1362 if show: 1363 uLogger.info(infoTextShort) 1364 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1365 1366 if self.searchResultsFile: 1367 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1368 fH.write(infoText) 1369 1370 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1371 1372 return searchResults 1373 1374 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1375 """ 1376 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1377 1378 :param instruments: list of strings with tickers or FIGIs. 1379 :return: list with unique instrument FIGIs only. 1380 """ 1381 requestedInstruments = [] 1382 for iName in instruments: 1383 if iName not in self.aliases.keys(): 1384 if iName not in requestedInstruments: 1385 requestedInstruments.append(iName) 1386 1387 else: 1388 if iName not in requestedInstruments: 1389 if self.aliases[iName] not in requestedInstruments: 1390 requestedInstruments.append(self.aliases[iName]) 1391 1392 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1393 1394 onlyUniqueFIGIs = [] 1395 for iName in requestedInstruments: 1396 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1397 continue 1398 1399 self.ticker = iName 1400 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1401 1402 if not iData: 1403 self.ticker = "" 1404 self.figi = iName 1405 1406 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1407 1408 if not iData: 1409 self.figi = "" 1410 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1411 1412 if iData and iData["figi"] not in onlyUniqueFIGIs: 1413 onlyUniqueFIGIs.append(iData["figi"]) 1414 1415 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1416 1417 return onlyUniqueFIGIs 1418 1419 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1420 """ 1421 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1422 1423 See limits: https://tinkoff.github.io/investAPI/limits/ 1424 1425 If `pricesFile` string is not empty then also save information to this file. 1426 1427 :param instruments: list of strings with tickers or FIGIs. 1428 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1429 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1430 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1431 """ 1432 if instruments is None or not instruments: 1433 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1434 raise Exception("Ticker or FIGI required") 1435 1436 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1437 1438 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1439 1440 iList = [] # trying to get info and current prices about all unique instruments: 1441 for self.figi in onlyUniqueFIGIs: 1442 iData = self.SearchByFIGI(requestPrice=True) 1443 iList.append(iData) 1444 1445 self.ShowListOfPrices(iList, show) 1446 1447 return iList 1448 1449 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1450 """ 1451 Show table contains current prices of given instruments. 1452 1453 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1454 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1455 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1456 :return: multilines text in Markdown format as a table contains current prices. 1457 """ 1458 infoText = "" 1459 1460 if show or self.pricesFile: 1461 info = [ 1462 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1463 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1464 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1465 ] 1466 1467 for item in iList: 1468 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1469 item["ticker"], 1470 item["figi"], 1471 item["type"], 1472 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1473 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1474 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1475 "{} / {}".format( 1476 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1477 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1478 ), 1479 "{} / {}".format( 1480 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1481 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1482 ), 1483 item["currency"], 1484 )) 1485 1486 infoText = "".join(info) 1487 1488 if show: 1489 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1490 1491 if self.pricesFile: 1492 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1493 fH.write(infoText) 1494 1495 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1496 1497 return infoText 1498 1499 def RequestTradingStatus(self) -> dict: 1500 """ 1501 Requesting trading status for the instrument defined by `figi` variable. 1502 1503 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1504 1505 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1506 1507 :return: dictionary with trading status attributes. Response example: 1508 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1509 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1510 """ 1511 if self.figi is None or not self.figi: 1512 uLogger.error("Variable `figi` must be defined for using this method!") 1513 raise Exception("FIGI required") 1514 1515 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1516 1517 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1518 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1519 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1520 1521 if self.moreDebug: 1522 uLogger.debug("Records about current trading status successfully received") 1523 1524 return tradingStatus 1525 1526 def RequestPortfolio(self) -> dict: 1527 """ 1528 Requesting actual user's portfolio for current `accountId`. 1529 1530 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1531 1532 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1533 1534 :return: dictionary with user's portfolio. 1535 """ 1536 if self.accountId is None or not self.accountId: 1537 uLogger.error("Variable `accountId` must be defined for using this method!") 1538 raise Exception("Account ID required") 1539 1540 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1541 1542 self.body = str({"accountId": self.accountId}) 1543 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1544 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1545 1546 if self.moreDebug: 1547 uLogger.debug("Records about user's portfolio successfully received") 1548 1549 return rawPortfolio 1550 1551 def RequestPositions(self) -> dict: 1552 """ 1553 Requesting open positions by currencies and instruments for current `accountId`. 1554 1555 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1556 1557 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1558 1559 :return: dictionary with open positions by instruments. 1560 """ 1561 if self.accountId is None or not self.accountId: 1562 uLogger.error("Variable `accountId` must be defined for using this method!") 1563 raise Exception("Account ID required") 1564 1565 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1566 1567 self.body = str({"accountId": self.accountId}) 1568 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1569 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1570 1571 if self.moreDebug: 1572 uLogger.debug("Records about current open positions successfully received") 1573 1574 return rawPositions 1575 1576 def RequestPendingOrders(self) -> list: 1577 """ 1578 Requesting current actual pending orders for current `accountId`. 1579 1580 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1581 1582 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1583 1584 :return: list of dictionaries with pending orders. 1585 """ 1586 if self.accountId is None or not self.accountId: 1587 uLogger.error("Variable `accountId` must be defined for using this method!") 1588 raise Exception("Account ID required") 1589 1590 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1591 1592 self.body = str({"accountId": self.accountId}) 1593 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1594 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1595 1596 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1597 1598 return rawOrders 1599 1600 def RequestStopOrders(self) -> list: 1601 """ 1602 Requesting current actual stop orders for current `accountId`. 1603 1604 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1605 1606 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1607 1608 :return: list of dictionaries with stop orders. 1609 """ 1610 if self.accountId is None or not self.accountId: 1611 uLogger.error("Variable `accountId` must be defined for using this method!") 1612 raise Exception("Account ID required") 1613 1614 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1615 1616 self.body = str({"accountId": self.accountId}) 1617 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1618 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1619 1620 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1621 1622 return rawStopOrders 1623 1624 def Overview(self, show: bool = False, details: str = "full") -> dict: 1625 """ 1626 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1627 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1628 and `overviewBondsCalendarFile` are defined then also save information to file. 1629 1630 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1631 many requests about the state of the portfolio, and then, based on the received data, a large number 1632 of calculation and statistics are collected. 1633 1634 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1635 :param details: how detailed should the information be? 1636 - `full` — shows full available information about portfolio status (by default), 1637 - `positions` — shows only open positions, 1638 - `orders` — shows only sections of open limits and stop orders. 1639 - `digest` — show a short digest of the portfolio status, 1640 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1641 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1642 :return: dictionary with client's raw portfolio and some statistics. 1643 """ 1644 if self.accountId is None or not self.accountId: 1645 uLogger.error("Variable `accountId` must be defined for using this method!") 1646 raise Exception("Account ID required") 1647 1648 view = { 1649 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1650 "headers": {}, # list of dictionaries, response headers without "positions" section 1651 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1652 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1653 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1654 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1655 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1656 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1657 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1658 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1659 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1660 }, 1661 "stat": { # --- some statistics calculated using "raw" sections: 1662 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1663 "availableRUB": 0., # available rubles (without other currencies) 1664 "blockedRUB": 0., # blocked sum in Russian Rouble 1665 "totalChangesRUB": 0., # changes for all open trades in RUB 1666 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1667 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1668 "sharesCostRUB": 0., # costs of all shares in RUB 1669 "bondsCostRUB": 0., # costs of all bonds in RUB 1670 "etfsCostRUB": 0., # costs of all etfs in RUB 1671 "futuresCostRUB": 0., # costs of all futures in RUB 1672 "Currencies": [], # list of dictionaries of all currencies statistics 1673 "Shares": [], # list of dictionaries of all shares statistics 1674 "Bonds": [], # list of dictionaries of all bonds statistics 1675 "Etfs": [], # list of dictionaries of all etfs statistics 1676 "Futures": [], # list of dictionaries of all futures statistics 1677 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1678 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1679 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1680 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1681 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1682 }, 1683 "analytics": { # --- some analytics of portfolio: 1684 "distrByAssets": {}, # portfolio distribution by assets 1685 "distrByCompanies": {}, # portfolio distribution by companies 1686 "distrBySectors": {}, # portfolio distribution by sectors 1687 "distrByCurrencies": {}, # portfolio distribution by currencies 1688 "distrByCountries": {}, # portfolio distribution by countries 1689 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1690 } 1691 } 1692 1693 details = details.lower() 1694 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1695 if details not in availableDetails: 1696 details = "full" 1697 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1698 1699 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1700 1701 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1702 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1703 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1704 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1705 1706 # save response headers without "positions" section: 1707 for key in portfolioResponse.keys(): 1708 if key != "positions": 1709 view["raw"]["headers"][key] = portfolioResponse[key] 1710 1711 else: 1712 continue 1713 1714 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1715 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1716 for item in portfolioResponse["positions"]: 1717 if item["instrumentType"] == "currency": 1718 self.figi = item["figi"] 1719 curr = self.SearchByFIGI(requestPrice=False) 1720 1721 # current price of currency in RUB: 1722 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1723 "name": curr["name"], 1724 "currentPrice": NanoToFloat( 1725 item["currentPrice"]["units"], 1726 item["currentPrice"]["nano"] 1727 ), 1728 } 1729 1730 view["raw"]["Currencies"].append(item) 1731 1732 elif item["instrumentType"] == "share": 1733 view["raw"]["Shares"].append(item) 1734 1735 elif item["instrumentType"] == "bond": 1736 view["raw"]["Bonds"].append(item) 1737 1738 elif item["instrumentType"] == "etf": 1739 view["raw"]["Etfs"].append(item) 1740 1741 elif item["instrumentType"] == "futures": 1742 view["raw"]["Futures"].append(item) 1743 1744 else: 1745 continue 1746 1747 # how many volume of currencies (by ISO currency name) are blocked: 1748 for item in view["raw"]["positions"]["blocked"]: 1749 blocked = NanoToFloat(item["units"], item["nano"]) 1750 if blocked > 0: 1751 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1752 1753 # how many volume of instruments (by FIGI) are blocked: 1754 for item in view["raw"]["positions"]["securities"]: 1755 blocked = int(item["blocked"]) 1756 if blocked > 0: 1757 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1758 1759 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1760 1761 if "rub" in allBlocked.keys(): 1762 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1763 1764 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1765 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1766 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1767 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1768 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1769 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1770 view["stat"]["portfolioCostRUB"] = sum([ 1771 view["stat"]["allCurrenciesCostRUB"], 1772 view["stat"]["sharesCostRUB"], 1773 view["stat"]["bondsCostRUB"], 1774 view["stat"]["etfsCostRUB"], 1775 view["stat"]["futuresCostRUB"], 1776 ]) 1777 1778 # --- calculating some portfolio statistics: 1779 byComp = {} # distribution by companies 1780 bySect = {} # distribution by sectors 1781 byCurr = {} # distribution by currencies (include RUB) 1782 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1783 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1784 1785 for item in portfolioResponse["positions"]: 1786 self.figi = item["figi"] 1787 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1788 1789 if instrument: 1790 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1791 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1792 1793 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1794 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1795 1796 else: 1797 blocked = 0 1798 1799 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1800 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1801 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1802 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1803 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1804 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1805 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1806 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1807 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1808 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1809 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1810 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1811 1812 statData = { 1813 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1814 "ticker": instrument["ticker"], # ticker by FIGI 1815 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1816 "volume": volume, # available volume of instrument 1817 "lots": lots, # volume in lots of instrument 1818 "direction": direction, # direction of an instrument's position: short or long 1819 "blocked": blocked, # blocked volume of currency or instrument 1820 "currentPrice": curPrice, # current instrument's price in basic asset 1821 "average": average, # current average position price 1822 "cost": cost, # current cost of all volume of instrument in basic asset 1823 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1824 "costRUB": costRUB, # cost of instrument in ruble 1825 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1826 "profit": profit, # expected profit at current moment 1827 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1828 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1829 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1830 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1831 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1832 "step": instrument["step"], # minimum price increment 1833 } 1834 1835 # adding distribution by unique countries: 1836 if statData["country"] not in byCountry.keys(): 1837 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1838 1839 else: 1840 byCountry[statData["country"]]["cost"] += costRUB 1841 byCountry[statData["country"]]["percent"] += percentCostRUB 1842 1843 if item["instrumentType"] != "currency": 1844 # adding distribution by unique companies: 1845 if statData["name"]: 1846 if statData["name"] not in byComp.keys(): 1847 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1848 1849 else: 1850 byComp[statData["name"]]["cost"] += costRUB 1851 byComp[statData["name"]]["percent"] += percentCostRUB 1852 1853 # adding distribution by unique sectors: 1854 if statData["sector"] not in bySect.keys(): 1855 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1856 1857 else: 1858 bySect[statData["sector"]]["cost"] += costRUB 1859 bySect[statData["sector"]]["percent"] += percentCostRUB 1860 1861 # adding distribution by unique currencies: 1862 if currency not in byCurr.keys(): 1863 byCurr[currency] = { 1864 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1865 "cost": costRUB, 1866 "percent": percentCostRUB 1867 } 1868 1869 else: 1870 byCurr[currency]["cost"] += costRUB 1871 byCurr[currency]["percent"] += percentCostRUB 1872 1873 # saving statistics for every instrument: 1874 if item["instrumentType"] == "currency": 1875 view["stat"]["Currencies"].append(statData) 1876 1877 # update dict with free funds for trading (total - blocked) by currencies 1878 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1879 view["stat"]["funds"][currency] = { 1880 "total": volume, 1881 "totalCostRUB": costRUB, # total volume cost in rubles 1882 "free": volume - blocked, 1883 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1884 } 1885 1886 elif item["instrumentType"] == "share": 1887 view["stat"]["Shares"].append(statData) 1888 1889 elif item["instrumentType"] == "bond": 1890 view["stat"]["Bonds"].append(statData) 1891 1892 elif item["instrumentType"] == "etf": 1893 view["stat"]["Etfs"].append(statData) 1894 1895 elif item["instrumentType"] == "Futures": 1896 view["stat"]["Futures"].append(statData) 1897 1898 else: 1899 continue 1900 1901 # total changes in Russian Ruble: 1902 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1903 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1904 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1905 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1906 view["stat"]["funds"]["rub"] = { 1907 "total": view["stat"]["availableRUB"], 1908 "totalCostRUB": view["stat"]["availableRUB"], 1909 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1910 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1911 } 1912 1913 # --- pending orders sector data: 1914 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1915 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1916 1917 for item in view["raw"]["orders"]: 1918 self.figi = item["figi"] 1919 1920 if item["figi"] not in uniquePendingOrdersFIGIs: 1921 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1922 1923 uniquePendingOrdersFIGIs.append(item["figi"]) 1924 uniquePendingOrders[item["figi"]] = instrument 1925 1926 else: 1927 instrument = uniquePendingOrders[item["figi"]] 1928 1929 if instrument: 1930 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1931 orderType = TKS_ORDER_TYPES[item["orderType"]] 1932 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1933 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1934 1935 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1936 if item["direction"] == "ORDER_DIRECTION_BUY": 1937 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1938 1939 else: 1940 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1941 1942 # requested price for order execution: 1943 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1944 1945 # necessary changes in percent to reach target from current price: 1946 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1947 1948 view["stat"]["orders"].append({ 1949 "orderID": item["orderId"], # orderId number parameter of current order 1950 "figi": item["figi"], # FIGI identification 1951 "ticker": instrument["ticker"], # ticker name by FIGI 1952 "lotsRequested": item["lotsRequested"], # requested lots value 1953 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1954 "currentPrice": lastPrice, # current instrument's price for defined action 1955 "targetPrice": target, # requested price for order execution in base currency 1956 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1957 "percentChanges": changes, # changes in percent to target from current price 1958 "currency": item["currency"], # instrument's currency name 1959 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1960 "type": orderType, # type of order from TKS_ORDER_TYPES 1961 "status": orderState, # order status from TKS_ORDER_STATES 1962 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1963 }) 1964 1965 # --- stop orders sector data: 1966 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1967 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1968 1969 for item in view["raw"]["stopOrders"]: 1970 self.figi = item["figi"] 1971 1972 if item["figi"] not in uniqueStopOrdersFIGIs: 1973 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1974 1975 uniqueStopOrdersFIGIs.append(item["figi"]) 1976 uniqueStopOrders[item["figi"]] = instrument 1977 1978 else: 1979 instrument = uniqueStopOrders[item["figi"]] 1980 1981 if instrument: 1982 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1983 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1984 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1985 1986 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1987 if "expirationTime" in item.keys(): 1988 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1989 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1990 1991 else: 1992 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1993 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1994 1995 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1996 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1997 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1998 1999 else: 2000 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2001 2002 # requested price when stop-order executed: 2003 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2004 2005 # price for limit-order, set up when stop-order executed: 2006 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2007 2008 # necessary changes in percent to reach target from current price: 2009 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2010 2011 view["stat"]["stopOrders"].append({ 2012 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2013 "figi": item["figi"], # FIGI identification 2014 "ticker": instrument["ticker"], # ticker name by FIGI 2015 "lotsRequested": item["lotsRequested"], # requested lots value 2016 "currentPrice": lastPrice, # current instrument's price for defined action 2017 "targetPrice": target, # requested price for stop-order execution in base currency 2018 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2019 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2020 "percentChanges": changes, # changes in percent to target from current price 2021 "currency": item["currency"], # instrument's currency name 2022 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2023 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2024 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2025 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2026 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2027 }) 2028 2029 # --- calculating data for analytics section: 2030 # portfolio distribution by assets: 2031 view["analytics"]["distrByAssets"] = { 2032 "Ruble": { 2033 "uniques": 1, 2034 "cost": view["stat"]["availableRUB"], 2035 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2036 }, 2037 "Currencies": { 2038 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2039 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2040 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2041 }, 2042 "Shares": { 2043 "uniques": len(view["stat"]["Shares"]), 2044 "cost": view["stat"]["sharesCostRUB"], 2045 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2046 }, 2047 "Bonds": { 2048 "uniques": len(view["stat"]["Bonds"]), 2049 "cost": view["stat"]["bondsCostRUB"], 2050 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2051 }, 2052 "Etfs": { 2053 "uniques": len(view["stat"]["Etfs"]), 2054 "cost": view["stat"]["etfsCostRUB"], 2055 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2056 }, 2057 "Futures": { 2058 "uniques": len(view["stat"]["Futures"]), 2059 "cost": view["stat"]["futuresCostRUB"], 2060 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2061 }, 2062 } 2063 2064 # portfolio distribution by companies: 2065 view["analytics"]["distrByCompanies"]["All money cash"] = { 2066 "ticker": "", 2067 "cost": view["stat"]["allCurrenciesCostRUB"], 2068 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2069 } 2070 view["analytics"]["distrByCompanies"].update(byComp) 2071 2072 # portfolio distribution by sectors: 2073 view["analytics"]["distrBySectors"]["All money cash"] = { 2074 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2075 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2076 } 2077 view["analytics"]["distrBySectors"].update(bySect) 2078 2079 # portfolio distribution by currencies: 2080 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2081 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2082 2083 if self.moreDebug: 2084 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2085 2086 view["analytics"]["distrByCurrencies"].update(byCurr) 2087 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2088 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2089 2090 # portfolio distribution by countries: 2091 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2092 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2093 2094 if self.moreDebug: 2095 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2096 2097 view["analytics"]["distrByCountries"].update(byCountry) 2098 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2099 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2100 2101 # --- Prepare text statistics overview in human-readable: 2102 if show: 2103 # Whatever the value `details`, header not changes: 2104 info = [ 2105 "# Client's portfolio\n\n", 2106 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2107 "* **Account ID:** [{}]\n".format(self.accountId), 2108 ] 2109 2110 if details in ["full", "positions", "digest"]: 2111 info.extend([ 2112 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2113 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2114 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2115 view["stat"]["totalChangesRUB"], 2116 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2117 view["stat"]["totalChangesPercentRUB"], 2118 ), 2119 ]) 2120 2121 if details in ["full", "positions"]: 2122 info.extend([ 2123 "## Open positions\n\n", 2124 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2125 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2126 "| Ruble | {:>31} | | | | | |\n".format( 2127 "{:.2f} ({:.2f}) rub".format( 2128 view["stat"]["availableRUB"], 2129 view["stat"]["blockedRUB"], 2130 ) 2131 ) 2132 ]) 2133 2134 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2135 return [ 2136 "| | | | | | | |\n", 2137 "| {:<27} | | | | | {:>19} | |\n".format( 2138 noTradeStr if noTradeStr else typeStr, 2139 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2140 ), 2141 ] 2142 2143 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2144 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2145 "{} [{}]".format(data["ticker"], data["figi"]), 2146 "{:.2f} ({:.2f}) {}".format( 2147 data["volume"], 2148 data["blocked"], 2149 data["currency"], 2150 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2151 data["volume"], 2152 data["blocked"], 2153 ), 2154 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2155 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2156 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2157 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2158 "{}{:.2f} {} ({}{:.2f}%)".format( 2159 "+" if data["profit"] > 0 else "", 2160 data["profit"], data["baseCurrencyName"], 2161 "+" if data["percentProfit"] > 0 else "", 2162 data["percentProfit"], 2163 ), 2164 ) 2165 2166 # --- Show currencies section: 2167 if view["stat"]["Currencies"]: 2168 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2169 for item in view["stat"]["Currencies"]: 2170 info.append(_InfoStr(item, showCurrencyName=True)) 2171 2172 else: 2173 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2174 2175 # --- Show shares section: 2176 if view["stat"]["Shares"]: 2177 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2178 2179 for item in view["stat"]["Shares"]: 2180 info.append(_InfoStr(item)) 2181 2182 else: 2183 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2184 2185 # --- Show bonds section: 2186 if view["stat"]["Bonds"]: 2187 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2188 2189 for item in view["stat"]["Bonds"]: 2190 info.append(_InfoStr(item)) 2191 2192 else: 2193 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2194 2195 # --- Show etfs section: 2196 if view["stat"]["Etfs"]: 2197 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2198 2199 for item in view["stat"]["Etfs"]: 2200 info.append(_InfoStr(item)) 2201 2202 else: 2203 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2204 2205 # --- Show futures section: 2206 if view["stat"]["Futures"]: 2207 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2208 2209 for item in view["stat"]["Futures"]: 2210 info.append(_InfoStr(item)) 2211 2212 else: 2213 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2214 2215 if details in ["full", "orders"]: 2216 # --- Show pending orders section: 2217 if view["stat"]["orders"]: 2218 info.extend([ 2219 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2220 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2221 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2222 ]) 2223 2224 for item in view["stat"]["orders"]: 2225 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2226 "{} [{}]".format(item["ticker"], item["figi"]), 2227 item["orderID"], 2228 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2229 "{} {} ({}{:.2f}%)".format( 2230 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2231 item["baseCurrencyName"], 2232 "+" if item["percentChanges"] > 0 else "", 2233 float(item["percentChanges"]), 2234 ), 2235 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2236 item["action"], 2237 item["type"], 2238 item["date"], 2239 )) 2240 2241 else: 2242 info.append("\n## Total pending limit-orders: 0\n") 2243 2244 # --- Show stop orders section: 2245 if view["stat"]["stopOrders"]: 2246 info.extend([ 2247 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2248 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2249 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2250 ]) 2251 2252 for item in view["stat"]["stopOrders"]: 2253 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2254 "{} [{}]".format(item["ticker"], item["figi"]), 2255 item["orderID"], 2256 item["lotsRequested"], 2257 "{} {} ({}{:.2f}%)".format( 2258 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2259 item["baseCurrencyName"], 2260 "+" if item["percentChanges"] > 0 else "", 2261 float(item["percentChanges"]), 2262 ), 2263 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2264 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2265 item["action"], 2266 item["type"], 2267 item["expType"], 2268 item["createDate"], 2269 item["expDate"], 2270 )) 2271 2272 else: 2273 info.append("\n## Total stop-orders: 0\n") 2274 2275 if details in ["full", "analytics"]: 2276 # -- Show analytics section: 2277 if view["stat"]["portfolioCostRUB"] > 0: 2278 info.extend([ 2279 "\n# Analytics\n" 2280 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2281 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2282 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2283 view["stat"]["totalChangesRUB"], 2284 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2285 view["stat"]["totalChangesPercentRUB"], 2286 ), 2287 "\n## Portfolio distribution by assets\n" 2288 "\n| Type | Uniques | Percent | Current cost |\n", 2289 "|------------------------------------|---------|---------|--------------------|\n", 2290 ]) 2291 2292 for key in view["analytics"]["distrByAssets"].keys(): 2293 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2294 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2295 key, 2296 view["analytics"]["distrByAssets"][key]["uniques"], 2297 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2298 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2299 )) 2300 2301 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2302 2303 info.extend([ 2304 "\n## Portfolio distribution by companies\n" 2305 "\n| Company | Percent | Current cost |\n", 2306 aSepLine, 2307 ]) 2308 2309 for company in view["analytics"]["distrByCompanies"].keys(): 2310 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2311 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2312 "{}{}".format( 2313 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2314 company, 2315 ), 2316 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2317 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2318 )) 2319 2320 info.extend([ 2321 "\n## Portfolio distribution by sectors\n" 2322 "\n| Sector | Percent | Current cost |\n", 2323 aSepLine, 2324 ]) 2325 2326 for sector in view["analytics"]["distrBySectors"].keys(): 2327 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2328 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2329 sector, 2330 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2331 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2332 )) 2333 2334 info.extend([ 2335 "\n## Portfolio distribution by currencies\n" 2336 "\n| Instruments currencies | Percent | Current cost |\n", 2337 aSepLine, 2338 ]) 2339 2340 for curr in view["analytics"]["distrByCurrencies"].keys(): 2341 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2342 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2343 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2344 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2345 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2346 )) 2347 2348 info.extend([ 2349 "\n## Portfolio distribution by countries\n" 2350 "\n| Assets by country | Percent | Current cost |\n", 2351 aSepLine, 2352 ]) 2353 2354 for country in view["analytics"]["distrByCountries"].keys(): 2355 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2356 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2357 country, 2358 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2359 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2360 )) 2361 2362 if details in ["full", "calendar"]: 2363 # -- Show bonds payment calendar section: 2364 if view["stat"]["Bonds"]: 2365 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2366 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2367 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2368 2369 else: 2370 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2371 2372 infoText = "".join(info) 2373 2374 uLogger.info(infoText) 2375 2376 if details == "full" and self.overviewFile: 2377 filename = self.overviewFile 2378 2379 elif details == "digest" and self.overviewDigestFile: 2380 filename = self.overviewDigestFile 2381 2382 elif details == "positions" and self.overviewPositionsFile: 2383 filename = self.overviewPositionsFile 2384 2385 elif details == "orders" and self.overviewOrdersFile: 2386 filename = self.overviewOrdersFile 2387 2388 elif details == "analytics" and self.overviewAnalyticsFile: 2389 filename = self.overviewAnalyticsFile 2390 2391 elif details == "calendar" and self.overviewBondsCalendarFile: 2392 filename = self.overviewBondsCalendarFile 2393 2394 else: 2395 filename = "" 2396 2397 if filename: 2398 with open(filename, "w", encoding="UTF-8") as fH: 2399 fH.write(infoText) 2400 2401 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2402 2403 return view 2404 2405 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2406 """ 2407 Returns history operations between two given dates for current `accountId`. 2408 If `reportFile` string is not empty then also save human-readable report. 2409 Shows some statistical data of closed positions. 2410 2411 :param start: see docstring in `GetDatesAsString()` method 2412 :param end: see docstring in `GetDatesAsString()` method 2413 :param show: if `True` then also prints all records to the console. 2414 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2415 :return: original list of dictionaries with history of deals records from API ("operations" key): 2416 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2417 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2418 """ 2419 if self.accountId is None or not self.accountId: 2420 uLogger.error("Variable `accountId` must be defined for using this method!") 2421 raise Exception("Account ID required") 2422 2423 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2424 2425 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2426 2427 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2428 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2429 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2430 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2431 customStat = {} # custom statistics in additional to responseJSON 2432 2433 # --- output report in human-readable format: 2434 if show or self.reportFile: 2435 splitLine1 = "| | | | | |\n" # Summary section 2436 splitLine2 = "| | | | | | | | |\n" # Operations section 2437 nextDay = "" 2438 2439 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2440 2441 if len(ops) > 0: 2442 customStat = { 2443 "opsCount": 0, # total operations count 2444 "buyCount": 0, # buy operations 2445 "sellCount": 0, # sell operations 2446 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2447 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2448 "payIn": {"rub": 0.}, # Deposit brokerage account 2449 "payOut": {"rub": 0.}, # Withdrawals 2450 "divs": {"rub": 0.}, # Dividends income 2451 "coupons": {"rub": 0.}, # Coupon's income 2452 "brokerCom": {"rub": 0.}, # Service commissions 2453 "serviceCom": {"rub": 0.}, # Service commissions 2454 "marginCom": {"rub": 0.}, # Margin commissions 2455 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2456 } 2457 2458 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2459 for item in ops: 2460 if item["state"] == "OPERATION_STATE_EXECUTED": 2461 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2462 2463 # count buy operations: 2464 if "_BUY" in item["operationType"]: 2465 customStat["buyCount"] += 1 2466 2467 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2468 customStat["buyTotal"][item["payment"]["currency"]] += payment 2469 2470 else: 2471 customStat["buyTotal"][item["payment"]["currency"]] = payment 2472 2473 # count sell operations: 2474 elif "_SELL" in item["operationType"]: 2475 customStat["sellCount"] += 1 2476 2477 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2478 customStat["sellTotal"][item["payment"]["currency"]] += payment 2479 2480 else: 2481 customStat["sellTotal"][item["payment"]["currency"]] = payment 2482 2483 # count incoming operations: 2484 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2485 if item["payment"]["currency"] in customStat["payIn"].keys(): 2486 customStat["payIn"][item["payment"]["currency"]] += payment 2487 2488 else: 2489 customStat["payIn"][item["payment"]["currency"]] = payment 2490 2491 # count withdrawals operations: 2492 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2493 if item["payment"]["currency"] in customStat["payOut"].keys(): 2494 customStat["payOut"][item["payment"]["currency"]] += payment 2495 2496 else: 2497 customStat["payOut"][item["payment"]["currency"]] = payment 2498 2499 # count dividends income: 2500 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2501 if item["payment"]["currency"] in customStat["divs"].keys(): 2502 customStat["divs"][item["payment"]["currency"]] += payment 2503 2504 else: 2505 customStat["divs"][item["payment"]["currency"]] = payment 2506 2507 # count coupon's income: 2508 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2509 if item["payment"]["currency"] in customStat["coupons"].keys(): 2510 customStat["coupons"][item["payment"]["currency"]] += payment 2511 2512 else: 2513 customStat["coupons"][item["payment"]["currency"]] = payment 2514 2515 # count broker commissions: 2516 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2517 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2518 customStat["brokerCom"][item["payment"]["currency"]] += payment 2519 2520 else: 2521 customStat["brokerCom"][item["payment"]["currency"]] = payment 2522 2523 # count service commissions: 2524 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2525 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2526 customStat["serviceCom"][item["payment"]["currency"]] += payment 2527 2528 else: 2529 customStat["serviceCom"][item["payment"]["currency"]] = payment 2530 2531 # count margin commissions: 2532 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2533 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2534 customStat["marginCom"][item["payment"]["currency"]] += payment 2535 2536 else: 2537 customStat["marginCom"][item["payment"]["currency"]] = payment 2538 2539 # count withholding taxes: 2540 elif "_TAX" in item["operationType"]: 2541 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2542 customStat["allTaxes"][item["payment"]["currency"]] += payment 2543 2544 else: 2545 customStat["allTaxes"][item["payment"]["currency"]] = payment 2546 2547 else: 2548 continue 2549 2550 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2551 2552 # --- view "Actions" lines: 2553 info.extend([ 2554 "| Report sections | | | | |\n", 2555 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2556 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2557 "| | Buy: {:<22} | {:<28} | | |\n".format( 2558 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2559 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2560 ), 2561 "| | Sell: {:<21} | {:<28} | | |\n".format( 2562 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2563 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2564 ), 2565 ]) 2566 2567 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2568 for key in opsKeys: 2569 if key == "rub": 2570 continue 2571 2572 info.extend([ 2573 "| | | {:<28} | | |\n".format( 2574 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2575 ), 2576 "| | | {:<28} | | |\n".format( 2577 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2578 ), 2579 ]) 2580 2581 info.append(splitLine1) 2582 2583 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2584 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2585 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2586 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2587 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2588 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2589 ) 2590 2591 # --- view "Payments" lines: 2592 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2593 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2594 2595 for key in paymentsKeys: 2596 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2597 2598 info.append(splitLine1) 2599 2600 # --- view "Commissions and taxes" lines: 2601 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2602 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2603 2604 for key in comKeys: 2605 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2606 2607 info.append(splitLine1) 2608 2609 info.extend([ 2610 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2611 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2612 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2613 ]) 2614 2615 else: 2616 info.append("Broker returned no operations during this period\n") 2617 2618 # --- view "Operations" section: 2619 for item in ops: 2620 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2621 continue 2622 2623 else: 2624 self.figi = item["figi"] if item["figi"] else "" 2625 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2626 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2627 2628 # group of deals during one day: 2629 if nextDay and item["date"].split("T")[0] != nextDay: 2630 info.append(splitLine2) 2631 nextDay = "" 2632 2633 else: 2634 nextDay = item["date"].split("T")[0] # saving current day for splitting 2635 2636 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2637 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2638 self.figi if self.figi else "—", 2639 instrument["ticker"] if instrument else "—", 2640 instrument["type"] if instrument else "—", 2641 item["quantity"] if int(item["quantity"]) > 0 else "—", 2642 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2643 TKS_OPERATION_STATES[item["state"]], 2644 TKS_OPERATION_TYPES[item["operationType"]], 2645 )) 2646 2647 infoText = "".join(info) 2648 2649 if show: 2650 if self.moreDebug: 2651 uLogger.debug("Records about history of a client's operations successfully received") 2652 2653 uLogger.info(infoText) 2654 2655 if self.reportFile: 2656 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2657 fH.write(infoText) 2658 2659 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2660 2661 return ops, customStat 2662 2663 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2664 """ 2665 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2666 2667 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2668 Warning! Broker server used ISO UTC time by default. 2669 2670 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2671 Also, `historyFile` used to update history with `onlyMissing` parameter. 2672 2673 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2674 2675 :param start: see docstring in `GetDatesAsString()` method. 2676 :param end: see docstring in `GetDatesAsString()` method. 2677 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2678 `"hour"`, `"day"`. Default: `"hour"`. 2679 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2680 False by default. Warning! History appends only from last candle to current time 2681 with always update last candle! 2682 :param csvSep: separator if csv-file is used, `,` by default. 2683 :param show: if `True` then also prints Pandas DataFrame to the console. 2684 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2685 `["date", "time", "open", "high", "low", "close", "volume"]`. 2686 """ 2687 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2688 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2689 history = None # empty pandas object for history 2690 2691 if interval not in TKS_CANDLE_INTERVALS.keys(): 2692 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2693 raise Exception("Incorrect value") 2694 2695 if not (self.ticker or self.figi): 2696 uLogger.error("Ticker or FIGI must be defined!") 2697 raise Exception("Ticker or FIGI required") 2698 2699 if self.ticker and not self.figi: 2700 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2701 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2702 2703 if self.figi and not self.ticker: 2704 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2705 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2706 2707 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2708 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2709 if interval.lower() != "day": 2710 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2711 2712 delta = dtEnd - dtStart # current UTC time minus last time in file 2713 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2714 2715 # calculate history length in candles: 2716 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2717 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2718 length += 1 # to avoid fraction time 2719 2720 # calculate data blocks count: 2721 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2722 2723 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2724 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2725 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2726 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2727 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2728 2729 tempOld = None # pandas object for old history, if --only-missing key present 2730 lastTime = None # datetime object of last old candle in file 2731 2732 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2733 uLogger.debug("--only-missing key present, add only last missing candles...") 2734 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2735 2736 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2737 2738 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2739 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2740 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2741 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2742 2743 # get last datetime object from last string in file or minus 1 delta if file is empty: 2744 if len(tempOld) > 0: 2745 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2746 2747 else: 2748 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2749 2750 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2751 2752 responseJSONs = [] # raw history blocks of data 2753 2754 blockEnd = dtEnd 2755 for item in range(blocks): 2756 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2757 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2758 2759 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2760 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2761 )) 2762 2763 if blockStart == blockEnd: 2764 uLogger.debug("Skipped this zero-length block...") 2765 2766 else: 2767 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2768 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2769 self.body = str({ 2770 "figi": self.figi, 2771 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2772 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2773 "interval": TKS_CANDLE_INTERVALS[interval][0] 2774 }) 2775 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2776 2777 if "code" in responseJSON.keys(): 2778 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2779 2780 else: 2781 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2782 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2783 2784 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2785 2786 blockEnd = blockStart 2787 2788 printCount = len(responseJSONs) # candles to show in console 2789 if responseJSONs: 2790 tempHistory = pd.DataFrame( 2791 data={ 2792 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2793 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2794 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2795 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2796 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2797 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2798 "volume": [int(item["volume"]) for item in responseJSONs], 2799 }, 2800 index=range(len(responseJSONs)), 2801 columns=["date", "time", "open", "high", "low", "close", "volume"], 2802 ) 2803 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2804 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2805 2806 # append only newest candles to old history if --only-missing key present: 2807 if onlyMissing and tempOld is not None and lastTime is not None: 2808 index = 0 # find start index in tempHistory data: 2809 2810 for i, item in tempHistory.iterrows(): 2811 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2812 2813 if curTime == lastTime: 2814 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2815 index = i 2816 printCount = index + 1 2817 break 2818 2819 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2820 2821 else: 2822 history = tempHistory # if no `--only-missing` key then load full data from server 2823 2824 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2825 2826 if history is not None and not history.empty: 2827 if show: 2828 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2829 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2830 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2831 )) 2832 2833 else: 2834 uLogger.warning("Received an empty candles history!") 2835 2836 if self.historyFile is not None: 2837 if history is not None and not history.empty: 2838 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2839 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2840 2841 else: 2842 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2843 2844 else: 2845 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2846 2847 return history 2848 2849 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2850 """ 2851 Load candles history from csv-file and return Pandas DataFrame object. 2852 2853 See also: `History()` and `ShowHistoryChart()` methods. 2854 2855 :param filePath: path to csv-file to open. 2856 """ 2857 loadedHistory = None # init candles data object 2858 2859 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2860 2861 if os.path.exists(filePath): 2862 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2863 2864 tfStr = self.priceModel.FormattedDelta( 2865 self.priceModel.timeframe, 2866 "{days} days {hours}h {minutes}m {seconds}s", 2867 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2868 self.priceModel.timeframe, 2869 "{hours}h {minutes}m {seconds}s", 2870 ) 2871 2872 if loadedHistory is not None and not loadedHistory.empty: 2873 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2874 len(loadedHistory), 2875 tfStr, 2876 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2877 ) 2878 2879 else: 2880 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2881 2882 else: 2883 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2884 2885 return loadedHistory 2886 2887 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2888 """ 2889 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2890 2891 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2892 Default: `index.html` (both for interact and non-interact candlesticks chart). 2893 2894 See also: `History()` and `LoadHistory()` methods. 2895 2896 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2897 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2898 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2899 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2900 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2901 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2902 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2903 """ 2904 if isinstance(candles, str): 2905 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2906 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2907 2908 elif isinstance(candles, pd.DataFrame): 2909 self.priceModel.prices = candles # set candles chain from variable 2910 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2911 2912 if "datetime" not in candles.columns: 2913 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2914 2915 else: 2916 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2917 raise Exception("Incorrect value") 2918 2919 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2920 2921 if interact: 2922 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2923 2924 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2925 2926 else: 2927 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2928 2929 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2930 2931 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2932 2933 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2934 """ 2935 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2936 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2937 2938 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2939 2940 :param operation: string "Buy" or "Sell". 2941 :param lots: volume, integer count of lots >= 1. 2942 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2943 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2944 :param expDate: string "Undefined" by default or local date in future, 2945 it is a string with format `%Y-%m-%d %H:%M:%S`. 2946 :return: JSON with response from broker server. 2947 """ 2948 if self.accountId is None or not self.accountId: 2949 uLogger.error("Variable `accountId` must be defined for using this method!") 2950 raise Exception("Account ID required") 2951 2952 if operation is None or not operation or operation not in ("Buy", "Sell"): 2953 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2954 raise Exception("Incorrect value") 2955 2956 if lots is None or lots < 1: 2957 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2958 lots = 1 2959 2960 if tp is None or tp < 0: 2961 tp = 0 2962 2963 if sl is None or sl < 0: 2964 sl = 0 2965 2966 if expDate is None or not expDate: 2967 expDate = "Undefined" 2968 2969 if not (self.ticker or self.figi): 2970 uLogger.error("Ticker or FIGI must be defined!") 2971 raise Exception("Ticker or FIGI required") 2972 2973 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2974 self.ticker = instrument["ticker"] 2975 self.figi = instrument["figi"] 2976 2977 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2978 2979 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2980 self.body = str({ 2981 "figi": self.figi, 2982 "quantity": str(lots), 2983 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2984 "accountId": str(self.accountId), 2985 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2986 }) 2987 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2988 2989 if "orderId" in response.keys(): 2990 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2991 operation, response["orderId"], 2992 self.ticker, self.figi, lots, 2993 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2994 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2995 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2996 )) 2997 2998 if tp > 0: 2999 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3000 3001 if sl > 0: 3002 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3003 3004 else: 3005 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 3006 3007 return response 3008 3009 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3010 """ 3011 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3012 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3013 3014 See also: `Order()` and `Trade()` docstrings. 3015 3016 :param lots: volume, integer count of lots >= 1. 3017 :param tp: float > 0, take profit price of stop-order. 3018 :param sl: float > 0, stop loss price of stop-order. 3019 :param expDate: it's a local date in future. 3020 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3021 :return: JSON with response from broker server. 3022 """ 3023 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3024 3025 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3026 """ 3027 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3028 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3029 3030 See also: `Order()` and `Trade()` docstrings. 3031 3032 :param lots: volume, integer count of lots >= 1. 3033 :param tp: float > 0, take profit price of stop-order. 3034 :param sl: float > 0, stop loss price of stop-order. 3035 :param expDate: it's a local date in the future. 3036 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3037 :return: JSON with response from broker server. 3038 """ 3039 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3040 3041 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3042 """ 3043 Close position of given instruments. 3044 3045 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3046 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3047 This avoids unnecessary downloading data from the server. 3048 """ 3049 if instruments is None or not instruments: 3050 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3051 raise Exception("Ticker or FIGI required") 3052 3053 if isinstance(instruments, str): 3054 instruments = [instruments] 3055 3056 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3057 if uniqueInstruments: 3058 if portfolio is None or not portfolio: 3059 portfolio = self.Overview(show=False) 3060 3061 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3062 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3063 3064 for self.figi in uniqueInstruments: 3065 if self.figi not in allOpened: 3066 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3067 continue 3068 3069 # search open trade info about instrument by ticker: 3070 instrument = {} 3071 for iType in TKS_INSTRUMENTS: 3072 if instrument: 3073 break 3074 3075 for item in portfolio["stat"][iType]: 3076 if item["figi"] == self.figi: 3077 instrument = item 3078 break 3079 3080 if instrument: 3081 self.ticker = instrument["ticker"] 3082 self.figi = instrument["figi"] 3083 3084 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3085 self.ticker, 3086 self.figi, 3087 int(instrument["volume"]), 3088 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3089 )) 3090 3091 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3092 3093 if tradeLots > 0: 3094 if instrument["blocked"] > 0: 3095 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3096 instrument["blocked"], 3097 self.ticker, 3098 tradeLots, 3099 )) 3100 3101 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3102 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3103 3104 else: 3105 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3106 3107 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3108 """ 3109 Close all positions of given instruments with defined type. 3110 3111 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3112 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3113 This avoids unnecessary downloading data from the server. 3114 """ 3115 if iType not in TKS_INSTRUMENTS: 3116 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3117 3118 else: 3119 if portfolio is None or not portfolio: 3120 portfolio = self.Overview(show=False) 3121 3122 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3123 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3124 3125 if tickers and portfolio: 3126 self.CloseTrades(tickers, portfolio) 3127 3128 else: 3129 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3130 3131 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3132 """ 3133 Universal method to create market or limit orders with all available parameters for current `accountId`. 3134 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3135 3136 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3137 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3138 3139 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3140 then broker immediately open market order as you can do simple --buy or --sell operations! 3141 3142 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3143 When current price will go up or down to target price value then broker opens a limit order. 3144 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3145 3146 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3147 3148 :param operation: string "Buy" or "Sell". 3149 :param orderType: string "Limit" or "Stop". 3150 :param lots: volume, integer count of lots >= 1. 3151 :param targetPrice: target price > 0. This is open trade price for limit order. 3152 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3153 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3154 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3155 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3156 Stop loss order always executed by market price. 3157 :param expDate: string "Undefined" by default or local date in future. 3158 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3159 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3160 A limit order has no expiration date, it lasts until the end of the trading day. 3161 :return: JSON with response from broker server. 3162 """ 3163 if self.accountId is None or not self.accountId: 3164 uLogger.error("Variable `accountId` must be defined for using this method!") 3165 raise Exception("Account ID required") 3166 3167 if operation is None or not operation or operation not in ("Buy", "Sell"): 3168 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3169 raise Exception("Incorrect value") 3170 3171 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3172 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3173 raise Exception("Incorrect value") 3174 3175 if lots is None or lots < 1: 3176 uLogger.error("You must define trade volume > 0: integer count of lots!") 3177 raise Exception("Incorrect value") 3178 3179 if targetPrice is None or targetPrice <= 0: 3180 uLogger.error("Target price for limit-order must be greater than 0!") 3181 raise Exception("Incorrect value") 3182 3183 if limitPrice is None or limitPrice <= 0: 3184 limitPrice = targetPrice 3185 3186 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3187 stopType = "Limit" 3188 3189 if expDate is None or not expDate: 3190 expDate = "Undefined" 3191 3192 if not (self.ticker or self.figi): 3193 uLogger.error("Tocker or FIGI must be defined!") 3194 raise Exception("Ticker or FIGI required") 3195 3196 response = {} 3197 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3198 self.ticker = instrument["ticker"] 3199 self.figi = instrument["figi"] 3200 3201 if orderType == "Limit": 3202 uLogger.debug( 3203 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3204 self.ticker, self.figi, 3205 operation, lots, targetPrice, instrument["currency"], 3206 )) 3207 3208 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3209 self.body = str({ 3210 "figi": self.figi, 3211 "quantity": str(lots), 3212 "price": FloatToNano(targetPrice), 3213 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3214 "accountId": str(self.accountId), 3215 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3216 }) 3217 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3218 3219 if "orderId" in response.keys(): 3220 uLogger.info( 3221 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3222 response["orderId"], 3223 self.ticker, self.figi, 3224 operation, lots, targetPrice, instrument["currency"], 3225 )) 3226 3227 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3228 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3229 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3230 targetPrice, instrument["currency"], 3231 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3232 )) 3233 3234 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3235 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3236 targetPrice, instrument["currency"], 3237 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3238 )) 3239 3240 else: 3241 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3242 3243 if orderType == "Stop": 3244 uLogger.debug( 3245 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3246 self.ticker, self.figi, 3247 operation, lots, 3248 targetPrice, instrument["currency"], 3249 limitPrice, instrument["currency"], 3250 stopType, expDate, 3251 )) 3252 3253 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3254 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3255 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3256 3257 body = { 3258 "figi": self.figi, 3259 "quantity": str(lots), 3260 "price": FloatToNano(limitPrice), 3261 "stopPrice": FloatToNano(targetPrice), 3262 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3263 "accountId": str(self.accountId), 3264 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3265 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3266 } 3267 3268 if expDateUTC: 3269 body["expireDate"] = expDateUTC 3270 3271 self.body = str(body) 3272 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3273 3274 if "stopOrderId" in response.keys(): 3275 uLogger.info( 3276 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3277 response["stopOrderId"], 3278 self.ticker, self.figi, 3279 operation, lots, 3280 targetPrice, instrument["currency"], 3281 limitPrice, instrument["currency"], 3282 TKS_STOP_ORDER_TYPES[stopOrderType], 3283 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3284 )) 3285 3286 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3287 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3288 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3289 targetPrice, instrument["currency"], 3290 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3291 )) 3292 3293 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3294 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3295 targetPrice, instrument["currency"], 3296 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3297 )) 3298 3299 else: 3300 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3301 3302 return response 3303 3304 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3305 """ 3306 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3307 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3308 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3309 See also: `Order()` docstring. 3310 3311 :param lots: volume, integer count of lots >= 1. 3312 :param targetPrice: target price > 0. This is open trade price for limit order. 3313 :return: JSON with response from broker server. 3314 """ 3315 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3316 3317 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3318 """ 3319 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3320 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3321 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3322 target price value then broker opens a limit order. See also: `Order()` docstring. 3323 3324 :param lots: volume, integer count of lots >= 1. 3325 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3326 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3327 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3328 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3329 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3330 :param expDate: string "Undefined" by default or local date in future. 3331 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3332 This date is converting to UTC format for server. 3333 :return: JSON with response from broker server. 3334 """ 3335 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3336 3337 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3338 """ 3339 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3340 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3341 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3342 See also: `Order()` docstring. 3343 3344 :param lots: volume, integer count of lots >= 1. 3345 :param targetPrice: target price > 0. This is open trade price for limit order. 3346 :return: JSON with response from broker server. 3347 """ 3348 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3349 3350 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3351 """ 3352 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3353 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3354 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3355 target price value then broker opens a limit order. See also: `Order()` docstring. 3356 3357 :param lots: volume, integer count of lots >= 1. 3358 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3359 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3360 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3361 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3362 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3363 :param expDate: string "Undefined" by default or local date in future. 3364 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3365 This date is converting to UTC format for server. 3366 :return: JSON with response from broker server. 3367 """ 3368 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3369 3370 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3371 """ 3372 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3373 3374 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3375 :param allOrdersIDs: pre-received lists of all active pending orders. 3376 This avoids unnecessary downloading data from the server. 3377 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3378 """ 3379 if self.accountId is None or not self.accountId: 3380 uLogger.error("Variable `accountId` must be defined for using this method!") 3381 raise Exception("Account ID required") 3382 3383 if orderIDs: 3384 if allOrdersIDs is None or not allOrdersIDs: 3385 rawOrders = self.RequestPendingOrders() 3386 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3387 3388 if allStopOrdersIDs is None or not allStopOrdersIDs: 3389 rawStopOrders = self.RequestStopOrders() 3390 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3391 3392 for orderID in orderIDs: 3393 idInPendingOrders = orderID in allOrdersIDs 3394 idInStopOrders = orderID in allStopOrdersIDs 3395 3396 if not (idInPendingOrders or idInStopOrders): 3397 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3398 continue 3399 3400 else: 3401 if idInPendingOrders: 3402 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3403 3404 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3405 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3406 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3407 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3408 3409 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3410 if self.moreDebug: 3411 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3412 3413 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3414 3415 else: 3416 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3417 3418 elif idInStopOrders: 3419 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3420 3421 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3422 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3423 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3424 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3425 3426 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3427 if self.moreDebug: 3428 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3429 3430 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3431 3432 else: 3433 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3434 3435 else: 3436 continue 3437 3438 def CloseAllOrders(self) -> None: 3439 """ 3440 Gets a list of open pending and stop orders and cancel it all. 3441 """ 3442 rawOrders = self.RequestPendingOrders() 3443 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3444 lenOrders = len(allOrdersIDs) 3445 3446 rawStopOrders = self.RequestStopOrders() 3447 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3448 lenSOrders = len(allStopOrdersIDs) 3449 3450 if lenOrders > 0 or lenSOrders > 0: 3451 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3452 3453 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3454 3455 else: 3456 uLogger.info("Orders not found, nothing to cancel.") 3457 3458 def CloseAll(self, *args) -> None: 3459 """ 3460 Close all available (not blocked) opened trades and orders. 3461 3462 Also, you can select one or more keywords case-insensitive: 3463 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3464 3465 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3466 """ 3467 overview = self.Overview(show=False) # get all open trades info 3468 3469 if len(args) == 0: 3470 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3471 self.CloseAllOrders() # close all pending and stop orders 3472 3473 for iType in TKS_INSTRUMENTS: 3474 if iType != "Currencies": 3475 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3476 3477 else: 3478 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3479 lowerArgs = [x.lower() for x in args] 3480 3481 if "orders" in lowerArgs: 3482 self.CloseAllOrders() # close all pending and stop orders 3483 3484 for iType in TKS_INSTRUMENTS: 3485 if iType.lower() in lowerArgs and iType != "Currencies": 3486 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3487 3488 @staticmethod 3489 def ParseOrderParameters(operation, **inputParameters): 3490 """ 3491 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3492 3493 :param operation: string "Buy" or "Sell". 3494 :param inputParameters: this is dict of strings that looks like this 3495 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3496 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3497 "prices" key: one or more prices to open limit-orders 3498 Counts of values in lots and prices lists must be equals! 3499 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3500 """ 3501 # TODO: update order grid work with api v2 3502 pass 3503 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3504 # 3505 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3506 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3507 # raise Exception("Incorrect value") 3508 # 3509 # if "l" in inputParameters.keys(): 3510 # inputParameters["lots"] = inputParameters.pop("l") 3511 # 3512 # if "p" in inputParameters.keys(): 3513 # inputParameters["prices"] = inputParameters.pop("p") 3514 # 3515 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3516 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3517 # raise Exception("Incorrect value") 3518 # 3519 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3520 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3521 # 3522 # if len(lots) != len(prices): 3523 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3524 # raise Exception("Incorrect value") 3525 # 3526 # uLogger.debug("Extracted parameters for orders:") 3527 # uLogger.debug("lots = {}".format(lots)) 3528 # uLogger.debug("prices = {}".format(prices)) 3529 # 3530 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3531 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3532 # uLogger.debug("Order parameters: {}".format(result)) 3533 # 3534 # return result 3535 3536 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3537 """ 3538 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3539 3540 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3541 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3542 """ 3543 result = False 3544 msg = "Instrument not defined!" 3545 3546 if portfolio is None or not portfolio: 3547 portfolio = self.Overview(show=False) 3548 3549 if self.ticker: 3550 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3551 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3552 3553 for iType in TKS_INSTRUMENTS: 3554 for instrument in portfolio["stat"][iType]: 3555 if instrument["ticker"] == self.ticker: 3556 result = True 3557 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3558 break 3559 3560 elif self.figi: 3561 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3562 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3563 3564 for iType in TKS_INSTRUMENTS: 3565 for instrument in portfolio["stat"][iType]: 3566 if instrument["figi"] == self.figi: 3567 result = True 3568 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3569 break 3570 3571 else: 3572 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3573 3574 uLogger.debug(msg) 3575 3576 return result 3577 3578 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3579 """ 3580 Returns instrument from the user's portfolio if it presents there. 3581 Instrument must be defined by `ticker` (highly priority) or `figi`. 3582 3583 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3584 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3585 """ 3586 result = None 3587 msg = "Instrument not defined!" 3588 3589 if portfolio is None or not portfolio: 3590 portfolio = self.Overview(show=False) 3591 3592 if self.ticker: 3593 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3594 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3595 3596 for iType in TKS_INSTRUMENTS: 3597 for instrument in portfolio["stat"][iType]: 3598 if instrument["ticker"] == self.ticker: 3599 result = instrument 3600 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3601 break 3602 3603 elif self.figi: 3604 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3605 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3606 3607 for iType in TKS_INSTRUMENTS: 3608 for instrument in portfolio["stat"][iType]: 3609 if instrument["figi"] == self.figi: 3610 result = instrument 3611 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3612 break 3613 3614 else: 3615 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3616 3617 uLogger.debug(msg) 3618 3619 return result 3620 3621 def RequestLimits(self) -> dict: 3622 """ 3623 Method for obtaining the available funds for withdrawal for current `accountId`. 3624 3625 See also: 3626 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3627 - `OverviewLimits()` method 3628 3629 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3630 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3631 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3632 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3633 """ 3634 if self.accountId is None or not self.accountId: 3635 uLogger.error("Variable `accountId` must be defined for using this method!") 3636 raise Exception("Account ID required") 3637 3638 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3639 3640 self.body = str({"accountId": self.accountId}) 3641 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3642 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3643 3644 if self.moreDebug: 3645 uLogger.debug("Records about available funds for withdrawal successfully received") 3646 3647 return rawLimits 3648 3649 def OverviewLimits(self, show: bool = False) -> dict: 3650 """ 3651 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3652 3653 See also: `RequestLimits()`. 3654 3655 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3656 :return: dict with raw parsed data from server and some calculated statistics about it. 3657 """ 3658 if self.accountId is None or not self.accountId: 3659 uLogger.error("Variable `accountId` must be defined for using this method!") 3660 raise Exception("Account ID required") 3661 3662 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3663 3664 view = { 3665 "rawLimits": rawLimits, 3666 "limits": { # parsed data for every currency: 3667 "money": { # this is an array of portfolio currency positions 3668 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3669 }, 3670 "blocked": { # this is an array of blocked currency 3671 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3672 }, 3673 "blockedGuarantee": { # this is locked money under collateral for futures 3674 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3675 }, 3676 }, 3677 } 3678 3679 # --- Prepare text table with limits in human-readable format: 3680 if show: 3681 info = [ 3682 "# Withdrawal limits\n\n", 3683 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3684 "* **Account ID:** [{}]\n".format(self.accountId), 3685 ] 3686 3687 if view["limits"]["money"]: 3688 info.extend([ 3689 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3690 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3691 ]) 3692 3693 else: 3694 info.append("\nNo withdrawal limits\n") 3695 3696 for curr in view["limits"]["money"].keys(): 3697 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3698 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3699 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3700 3701 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3702 "[{}]".format(curr), 3703 "{:.2f}".format(view["limits"]["money"][curr]), 3704 "{:.2f}".format(availableMoney), 3705 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3706 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3707 ) 3708 3709 if curr == "rub": 3710 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3711 3712 else: 3713 info.append(infoStr) 3714 3715 infoText = "".join(info) 3716 3717 uLogger.info(infoText) 3718 3719 if self.withdrawalLimitsFile: 3720 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3721 fH.write(infoText) 3722 3723 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3724 3725 return view 3726 3727 def RequestAccounts(self) -> dict: 3728 """ 3729 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3730 3731 See also: 3732 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3733 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3734 - `OverviewUserInfo()` method 3735 3736 :return: dict with raw data from server that contains accounts info. Example of dict: 3737 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3738 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3739 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3740 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3741 """ 3742 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3743 3744 self.body = str({}) 3745 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3746 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3747 3748 if self.moreDebug: 3749 uLogger.debug("Records about available accounts successfully received") 3750 3751 return rawAccounts 3752 3753 def RequestUserInfo(self) -> dict: 3754 """ 3755 Method for requesting common user's information. 3756 3757 See also: 3758 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3759 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3760 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3761 - `OverviewUserInfo()` method 3762 3763 :return: dict with raw data from server that contains user's information. Example of dict: 3764 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3765 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3766 """ 3767 uLogger.debug("Requesting common user's information. Wait, please...") 3768 3769 self.body = str({}) 3770 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3771 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3772 3773 if self.moreDebug: 3774 uLogger.debug("Records about current user successfully received") 3775 3776 return rawUserInfo 3777 3778 def RequestMarginStatus(self, accountId: str = None) -> dict: 3779 """ 3780 Method for requesting margin calculation for defined account ID. 3781 3782 See also: 3783 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3784 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3785 - `OverviewUserInfo()` method 3786 3787 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3788 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3789 Example of responses: 3790 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3791 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3792 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3793 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3794 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3795 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3796 """ 3797 if accountId is None or not accountId: 3798 if self.accountId is None or not self.accountId: 3799 uLogger.error("Variable `accountId` must be defined for using this method!") 3800 raise Exception("Account ID required") 3801 3802 else: 3803 accountId = self.accountId # use `self.accountId` (main ID) by default 3804 3805 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3806 3807 self.body = str({"accountId": accountId}) 3808 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3809 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3810 3811 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3812 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3813 rawMargin = {} 3814 3815 else: 3816 if self.moreDebug: 3817 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3818 3819 return rawMargin 3820 3821 def RequestTariffLimits(self) -> dict: 3822 """ 3823 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3824 3825 See also: 3826 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3827 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3828 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3829 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3830 - `OverviewUserInfo()` method 3831 3832 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3833 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3834 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3835 """ 3836 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3837 3838 self.body = str({}) 3839 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3840 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3841 3842 if self.moreDebug: 3843 uLogger.debug("Records with limits of current tariff successfully received") 3844 3845 return rawTariffLimits 3846 3847 def RequestBondCoupons(self, iJSON: dict) -> dict: 3848 """ 3849 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3850 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3851 All dates are in UTC timezone. 3852 3853 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3854 Documentation: 3855 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3856 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3857 3858 See also: `ExtendBondsData()`. 3859 3860 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3861 If raw iJSON is not data of bond then server returns an error [400] with message: 3862 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3863 :return: dictionary with bond payment calendar. Response example 3864 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3865 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3866 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3867 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3868 """ 3869 if iJSON["figi"] is None or not iJSON["figi"]: 3870 uLogger.error("FIGI must be defined for using this method!") 3871 raise Exception("FIGI required") 3872 3873 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3874 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3875 3876 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3877 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3878 self.figi, 3879 startDate, 3880 endDate, 3881 )) 3882 3883 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3884 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3885 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3886 3887 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3888 uLogger.warning("Instrument type is not bond!") 3889 3890 else: 3891 if self.moreDebug: 3892 uLogger.debug("Records about bond payment calendar successfully received") 3893 3894 return calendar 3895 3896 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3897 """ 3898 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3899 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3900 coupon yields, current yields and some statistics etc. 3901 3902 WARNING! This is too long operation if a lot of bonds requested from broker server. 3903 3904 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3905 3906 :param instruments: list of strings with tickers or FIGIs. 3907 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3908 for further used by data scientists or stock analytics. 3909 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3910 In XLSX-file and Pandas DataFrame fields mean: 3911 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3912 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3913 """ 3914 if instruments is None or not instruments: 3915 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3916 raise Exception("Ticker or FIGI required") 3917 3918 if isinstance(instruments, str): 3919 instruments = [instruments] 3920 3921 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3922 3923 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3924 3925 iCount = len(uniqueInstruments) 3926 tooLong = iCount >= 20 3927 if tooLong: 3928 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3929 3930 bonds = None 3931 for i, self.figi in enumerate(uniqueInstruments): 3932 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3933 3934 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3935 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3936 rawBond = self.SearchByFIGI(requestPrice=True) 3937 3938 # Widen raw data with UTC current time (iData["actualDateTime"]): 3939 actualDate = datetime.now(tzutc()) 3940 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3941 3942 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3943 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3944 3945 # Replace some values with human-readable: 3946 iData["nominalCurrency"] = iData["nominal"]["currency"] 3947 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3948 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3949 iData["aciCurrency"] = iData["aciValue"]["currency"] 3950 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3951 iData["issueSize"] = int(iData["issueSize"]) 3952 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3953 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3954 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3955 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3956 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3957 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3958 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3959 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3960 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3961 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3962 3963 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3964 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3965 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3966 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3967 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3968 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3969 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3970 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3971 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3972 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3973 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3974 3975 # Widen raw data with calendar data from `rawCalendar` values: 3976 calendarData = [] 3977 if "events" in iData["rawCalendar"].keys(): 3978 for item in iData["rawCalendar"]["events"]: 3979 calendarData.append({ 3980 "couponDate": item["couponDate"], 3981 "couponNumber": int(item["couponNumber"]), 3982 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3983 "payCurrency": item["payOneBond"]["currency"], 3984 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3985 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3986 "couponStartDate": item["couponStartDate"], 3987 "couponEndDate": item["couponEndDate"], 3988 "couponPeriod": item["couponPeriod"], 3989 }) 3990 3991 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3992 if "maturityDate" not in iData.keys(): 3993 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3994 3995 # Widen raw data with Coupon Rate. 3996 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3997 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3998 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3999 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4000 4001 # Widen raw data with Yield to Maturity (YTM) on current date. 4002 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4003 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4004 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4005 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4006 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4007 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4008 4009 iData["calendar"] = calendarData # adds calendar at the end 4010 4011 # Remove not used data: 4012 iData.pop("uid") 4013 iData.pop("positionUid") 4014 iData.pop("currentPrice") 4015 iData.pop("rawCalendar") 4016 4017 colNames = list(iData.keys()) 4018 if bonds is None: 4019 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4020 4021 else: 4022 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4023 4024 else: 4025 uLogger.warning("Instrument is not a bond!") 4026 4027 processed = round(100 * (i + 1) / iCount, 1) 4028 if tooLong and processed % 5 == 0: 4029 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4030 4031 else: 4032 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4033 4034 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4035 4036 # Saving bonds from Pandas DataFrame to XLSX sheet: 4037 if xlsx and self.bondsXLSXFile: 4038 with pd.ExcelWriter( 4039 path=self.bondsXLSXFile, 4040 date_format=TKS_DATE_FORMAT, 4041 datetime_format=TKS_DATE_TIME_FORMAT, 4042 mode="w", 4043 ) as writer: 4044 bonds.to_excel( 4045 writer, 4046 sheet_name="Extended bonds data", 4047 index=True, 4048 encoding="UTF-8", 4049 freeze_panes=(1, 1), 4050 ) # saving as XLSX-file with freeze first row and column as headers 4051 4052 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4053 4054 return bonds 4055 4056 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4057 """ 4058 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4059 4060 WARNING! This is too long operation if a lot of bonds requested from broker server. 4061 4062 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4063 4064 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4065 extended information about bonds: main info, current prices, bond payment calendar, 4066 coupon yields, current yields and some statistics etc. 4067 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4068 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4069 for further used by data scientists or stock analytics. 4070 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4071 """ 4072 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4073 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4074 4075 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4076 4077 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4078 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4079 calendar = None 4080 for bond in extBonds.iterrows(): 4081 for item in bond[1]["calendar"]: 4082 cData = { 4083 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4084 "couponDate": item["couponDate"], 4085 "figi": bond[1]["figi"], 4086 "ticker": bond[1]["ticker"], 4087 "name": bond[1]["name"], 4088 "couponNumber": item["couponNumber"], 4089 "payOneBond": item["payOneBond"], 4090 "payCurrency": item["payCurrency"], 4091 "couponType": item["couponType"], 4092 "couponPeriod": item["couponPeriod"], 4093 "fixDate": item["fixDate"], 4094 "couponStartDate": item["couponStartDate"], 4095 "couponEndDate": item["couponEndDate"], 4096 } 4097 4098 if calendar is None: 4099 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4100 4101 else: 4102 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4103 4104 if calendar is not None: 4105 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4106 4107 # Saving calendar from Pandas DataFrame to XLSX sheet: 4108 if xlsx: 4109 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4110 4111 with pd.ExcelWriter( 4112 path=xlsxCalendarFile, 4113 date_format=TKS_DATE_FORMAT, 4114 datetime_format=TKS_DATE_TIME_FORMAT, 4115 mode="w", 4116 ) as writer: 4117 humanReadable = calendar.copy(deep=True) 4118 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4119 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4120 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4121 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4122 humanReadable.columns = colNames # human-readable column names 4123 4124 humanReadable.to_excel( 4125 writer, 4126 sheet_name="Bond payments calendar", 4127 index=False, 4128 encoding="UTF-8", 4129 freeze_panes=(1, 2), 4130 ) # saving as XLSX-file with freeze first row and column as headers 4131 4132 del humanReadable # release df in memory 4133 4134 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4135 4136 return calendar 4137 4138 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4139 """ 4140 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4141 Also, creates Markdown file with calendar data, `calendar.md` by default. 4142 4143 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4144 4145 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4146 extended information about bonds: main info, current prices, bond payment calendar, 4147 coupon yields, current yields and some statistics etc. 4148 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4149 :param show: if `True` then also printing bonds payment calendar to the console, 4150 otherwise save to file `calendarFile` only. `False` by default. 4151 :return: multilines text in Markdown format with bonds payment calendar as a table. 4152 """ 4153 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4154 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4155 4156 infoText = "# Bond payments calendar\n\n" 4157 4158 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4159 4160 if not (calendar is None or calendar.empty): 4161 splitLine = "| | | | | | | | | |\n" 4162 4163 info = [ 4164 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4165 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4166 ] 4167 4168 newMonth = False 4169 notOneBond = calendar["figi"].nunique() > 1 4170 for i, bond in enumerate(calendar.iterrows()): 4171 if newMonth and notOneBond: 4172 info.append(splitLine) 4173 4174 info.append( 4175 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4176 " √" if bond[1]["paid"] else " —", 4177 bond[1]["couponDate"].split("T")[0], 4178 bond[1]["figi"], 4179 bond[1]["ticker"], 4180 bond[1]["couponNumber"], 4181 "{} {}".format( 4182 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4183 bond[1]["payCurrency"], 4184 ), 4185 bond[1]["couponType"], 4186 bond[1]["couponPeriod"], 4187 bond[1]["fixDate"].split("T")[0], 4188 ) 4189 ) 4190 4191 if i < len(calendar.values) - 1: 4192 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4193 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4194 newMonth = False if curDate.month == nextDate.month else True 4195 4196 else: 4197 newMonth = False 4198 4199 infoText += "".join(info) 4200 4201 if show: 4202 uLogger.info("{}".format(infoText)) 4203 4204 if self.calendarFile is not None: 4205 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4206 fH.write(infoText) 4207 4208 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4209 4210 else: 4211 infoText += "No data\n" 4212 4213 return infoText 4214 4215 def OverviewAccounts(self, show: bool = False) -> dict: 4216 """ 4217 Method for parsing and show simple table with all available user accounts. 4218 4219 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4220 4221 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4222 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4223 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4224 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4225 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4226 "closed": "—", "access": "Full access" }, ...}}` 4227 """ 4228 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4229 4230 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4231 accounts = { 4232 item["id"]: { 4233 "type": TKS_ACCOUNT_TYPES[item["type"]], 4234 "name": item["name"], 4235 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4236 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4237 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4238 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4239 } for item in rawAccounts["accounts"] 4240 } 4241 4242 # Raw and parsed data with some fields replaced in "stat" section: 4243 view = { 4244 "rawAccounts": rawAccounts, 4245 "stat": accounts, 4246 } 4247 4248 # --- Prepare simple text table with only accounts data in human-readable format: 4249 if show: 4250 info = [ 4251 "# User accounts\n\n", 4252 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4253 "| Account ID | Type | Status | Name |\n", 4254 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4255 ] 4256 4257 for account in view["stat"].keys(): 4258 info.extend([ 4259 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4260 account, 4261 view["stat"][account]["type"], 4262 view["stat"][account]["status"], 4263 view["stat"][account]["name"], 4264 ) 4265 ]) 4266 4267 infoText = "".join(info) 4268 4269 uLogger.info(infoText) 4270 4271 if self.userAccountsFile: 4272 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4273 fH.write(infoText) 4274 4275 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4276 4277 return view 4278 4279 def OverviewUserInfo(self, show: bool = False) -> dict: 4280 """ 4281 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4282 4283 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4284 4285 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4286 :return: dict with raw parsed data from server and some calculated statistics about it. 4287 """ 4288 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4289 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4290 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4291 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4292 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4293 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4294 4295 # This is dict with parsed common user data: 4296 userInfo = { 4297 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4298 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4299 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4300 "tariff": rawUserInfo["tariff"], 4301 } 4302 4303 # This is an array of dict with parsed margin statuses for every account IDs: 4304 margins = {} 4305 for accountId in accounts.keys(): 4306 if rawMargins[accountId]: 4307 margins[accountId] = { 4308 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4309 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4310 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4311 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4312 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4313 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4314 } 4315 4316 else: 4317 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4318 4319 unary = {} # unary-connection limits 4320 for item in rawTariffLimits["unaryLimits"]: 4321 if item["limitPerMinute"] in unary.keys(): 4322 unary[item["limitPerMinute"]].extend(item["methods"]) 4323 4324 else: 4325 unary[item["limitPerMinute"]] = item["methods"] 4326 4327 stream = {} # stream-connection limits 4328 for item in rawTariffLimits["streamLimits"]: 4329 if item["limit"] in stream.keys(): 4330 stream[item["limit"]].extend(item["streams"]) 4331 4332 else: 4333 stream[item["limit"]] = item["streams"] 4334 4335 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4336 limits = { 4337 "unary": unary, 4338 "stream": stream, 4339 } 4340 4341 # Raw and parsed data as an output result: 4342 view = { 4343 "rawUserInfo": rawUserInfo, 4344 "rawAccounts": rawAccounts, 4345 "rawMargins": rawMargins, 4346 "rawTariffLimits": rawTariffLimits, 4347 "stat": { 4348 "userInfo": userInfo, 4349 "accounts": accounts, 4350 "margins": margins, 4351 "limits": limits, 4352 }, 4353 } 4354 4355 # --- Prepare text table with user information in human-readable format: 4356 if show: 4357 info = [ 4358 "# Full user information\n\n", 4359 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4360 "## Common information\n\n", 4361 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4362 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4363 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4364 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4365 "\n## User accounts\n\n", 4366 ] 4367 4368 for account in view["stat"]["accounts"].keys(): 4369 info.extend([ 4370 "### ID: [{}]\n\n".format(account), 4371 "| Parameters | Values |\n", 4372 "|----------------------|--------------------------------------------------------------|\n", 4373 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4374 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4375 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4376 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4377 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4378 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4379 ]) 4380 4381 if margins[account]: 4382 info.extend([ 4383 "| Margin status: | Enabled |\n", 4384 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4385 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4386 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4387 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4388 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4389 ]) 4390 4391 else: 4392 info.append("| Margin status: | Disabled |\n\n") 4393 4394 info.extend([ 4395 "\n## Current user tariff limits\n", 4396 "\nSee also:\n", 4397 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4398 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4399 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4400 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4401 "\n### Unary limits\n", 4402 ]) 4403 4404 if unary: 4405 for key, values in sorted(unary.items()): 4406 info.append("\n* Max requests per minute: {}\n".format(key)) 4407 4408 for value in values: 4409 info.append(" - {}\n".format(value)) 4410 4411 else: 4412 info.append("\nNot available\n") 4413 4414 info.append("\n### Stream limits\n") 4415 4416 if stream: 4417 for key, values in sorted(stream.items()): 4418 info.append("\n* Max stream connections: {}\n".format(key)) 4419 4420 for value in values: 4421 info.append(" - {}\n".format(value)) 4422 4423 else: 4424 info.append("\nNot available\n") 4425 4426 infoText = "".join(info) 4427 4428 uLogger.info(infoText) 4429 4430 if self.userInfoFile: 4431 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4432 fH.write(infoText) 4433 4434 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4435 4436 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 253 254 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 255 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 256 257 See also: `SearchByTicker()`, `SearchInstruments()`. 258 """ 259 260 self.figi = "" 261 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 262 263 See also: `SearchByFIGI()`, `SearchInstruments()`. 264 """ 265 266 self.depth = 1 267 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 268 269 See also: `GetCurrentPrices()`. 270 """ 271 272 self.server = r"https://invest-public-api.tinkoff.ru/rest" 273 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 274 275 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 276 """ 277 278 uLogger.debug("Broker API server: {}".format(self.server)) 279 280 self.timeout = 15 281 """Server operations timeout in seconds. Default: `15`. 282 283 See also: `SendAPIRequest()`. 284 """ 285 286 self.headers = { 287 "Content-Type": "application/json", 288 "accept": "application/json", 289 "Authorization": "Bearer {}".format(self.token), 290 "x-app-name": "Tim55667757.TKSBrokerAPI", 291 } 292 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 293 294 See also: `SendAPIRequest()`. 295 """ 296 297 self.body = None 298 """Request body which send to broker server. Default: `None`. 299 300 See also: `SendAPIRequest()`. 301 """ 302 303 self.moreDebug = False 304 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 305 306 self.historyFile = None 307 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 308 309 See also: `History()`. 310 """ 311 312 self.htmlHistoryFile = "index.html" 313 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 314 315 See also: `ShowHistoryChart()`. 316 """ 317 318 self.instrumentsFile = "instruments.md" 319 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 320 321 See also: `ShowInstrumentsInfo()`. 322 """ 323 324 self.searchResultsFile = "search-results.md" 325 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 326 327 See also: `SearchInstruments()`. 328 """ 329 330 self.pricesFile = "prices.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `GetListOfPrices()`. 334 """ 335 336 self.infoFile = "info.md" 337 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 338 339 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 340 """ 341 342 self.bondsXLSXFile = "ext-bonds.xlsx" 343 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 344 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 345 346 See also: `ExtendBondsData()`. 347 """ 348 349 self.calendarFile = "calendar.md" 350 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 351 352 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 353 354 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 355 """ 356 357 self.overviewFile = "overview.md" 358 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 359 360 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 361 """ 362 363 self.overviewDigestFile = "overview-digest.md" 364 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 365 366 See also: `Overview()` with parameter `details="digest"`. 367 """ 368 369 self.overviewPositionsFile = "overview-positions.md" 370 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 371 372 See also: `Overview()` with parameter `details="positions"`. 373 """ 374 375 self.overviewOrdersFile = "overview-orders.md" 376 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 377 378 See also: `Overview()` with parameter `details="orders"`. 379 """ 380 381 self.overviewAnalyticsFile = "overview-analytics.md" 382 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 383 384 See also: `Overview()` with parameter `details="analytics"`. 385 """ 386 387 self.overviewBondsCalendarFile = "overview-calendar.md" 388 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 389 390 See also: `Overview()` with parameter `details="calendar"`. 391 """ 392 393 self.reportFile = "deals.md" 394 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 395 396 See also: `Deals()`. 397 """ 398 399 self.withdrawalLimitsFile = "limits.md" 400 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 401 402 See also: `OverviewLimits()` and `RequestLimits()`. 403 """ 404 405 self.userInfoFile = "user-info.md" 406 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 407 408 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 409 """ 410 411 self.userAccountsFile = "accounts.md" 412 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 413 414 See also: `OverviewAccounts()`, `RequestAccounts()`. 415 """ 416 417 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 418 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 419 420 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 421 422 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 423 """ 424 425 self.iList = None # init iList for raw instruments data 426 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 427 428 See also: `Listing()`, `DumpInstruments()`. 429 """ 430 431 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 432 if useCache: 433 if os.path.exists(self.iListDumpFile): 434 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 435 curTime = datetime.now(tzutc()) 436 437 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 438 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 439 440 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 441 442 else: 443 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 444 445 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 446 os.path.abspath(self.iListDumpFile), 447 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 448 )) 449 450 else: 451 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 452 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 453 454 else: 455 self.iList = self.Listing() # request new raw instruments data from broker server 456 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 457 458 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 459 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 460 461 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 462 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
478 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 479 """ 480 Send GET or POST request to broker server and receive JSON object. 481 482 self.header: must be defining with dictionary of headers. 483 self.body: if define then used as request body. None by default. 484 self.timeout: global request timeout, 15 seconds by default. 485 :param url: url with REST request. 486 :param reqType: send "GET" or "POST" request. "GET" by default. 487 :param retry: how many times retry after first request if an 5xx server errors occurred. 488 :param pause: sleep time in seconds between retries. 489 :return: response JSON (dictionary) from broker. 490 """ 491 if reqType not in ("GET", "POST"): 492 uLogger.error("You can define request type: 'GET' or 'POST'!") 493 raise Exception("Incorrect value") 494 495 if self.moreDebug: 496 uLogger.debug("Request parameters:") 497 uLogger.debug(" - REST API URL: {}".format(url)) 498 uLogger.debug(" - request type: {}".format(reqType)) 499 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 500 uLogger.debug(" - body:\n{}".format(self.body)) 501 502 # fast hack to avoid all operations with some tickers/FIGI 503 responseJSON = {} 504 oK = True 505 for item in self.exclude: 506 if item in url: 507 if self.moreDebug: 508 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 509 510 oK = False 511 break 512 513 if oK: 514 counter = 0 515 response = None 516 errMsg = "" 517 518 while not response and counter <= retry: 519 if reqType == "GET": 520 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 521 522 if reqType == "POST": 523 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 524 525 if self.moreDebug: 526 uLogger.debug("Response:") 527 uLogger.debug(" - status code: {}".format(response.status_code)) 528 uLogger.debug(" - reason: {}".format(response.reason)) 529 uLogger.debug(" - body length: {}".format(len(response.text))) 530 uLogger.debug(" - headers:\n{}".format(response.headers)) 531 532 # Server returns some headers: 533 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 534 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 535 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 536 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 537 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 538 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 539 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 540 sleep(rateLimitWait) 541 542 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 543 if 400 <= response.status_code < 500: 544 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 546 counter = retry + 1 547 548 if 500 <= response.status_code < 600: 549 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 550 uLogger.debug(" - not oK, {}".format(errMsg)) 551 counter += 1 552 553 if counter <= retry: 554 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 555 sleep(pause) 556 557 responseJSON = self._ParseJSON(rawData=response.text) 558 559 if errMsg: 560 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 561 uLogger.error(" - not oK, {}".format(errMsg)) 562 563 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
596 def Listing(self) -> dict: 597 """ 598 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 599 600 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 601 """ 602 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 603 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 604 605 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 606 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 607 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 608 609 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 610 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 611 poolUpdater.close() 612 613 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 614 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 615 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 616 617 # calculate minimum price increment (step) for all instruments and set up instrument's type: 618 for iType in iList.keys(): 619 for ticker in iList[iType]: 620 iList[iType][ticker]["type"] = iType 621 622 if "minPriceIncrement" in iList[iType][ticker].keys(): 623 iList[iType][ticker]["step"] = NanoToFloat( 624 iList[iType][ticker]["minPriceIncrement"]["units"], 625 iList[iType][ticker]["minPriceIncrement"]["nano"], 626 ) 627 628 else: 629 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 630 631 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
633 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 634 """ 635 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 636 637 See also: `DumpInstruments()`, `Listing()`. 638 639 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 640 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 641 """ 642 if self.iListDumpFile is None or not self.iListDumpFile: 643 uLogger.error("Output name of dump file must be defined!") 644 raise Exception("Filename required") 645 646 if not self.iList or forceUpdate: 647 self.iList = self.Listing() 648 649 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 650 651 # Save as XLSX with separated sheets for every type of instruments: 652 with pd.ExcelWriter( 653 path=xlsxDumpFile, 654 date_format=TKS_DATE_FORMAT, 655 datetime_format=TKS_DATE_TIME_FORMAT, 656 mode="w", 657 ) as writer: 658 for iType in TKS_INSTRUMENTS: 659 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 660 df = df[sorted(df)] # sorted by column names 661 df = df.applymap( 662 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 663 na_action="ignore", 664 ) # converting numbers from nano-type to float in every cell 665 df.to_excel( 666 writer, 667 sheet_name=iType, 668 encoding="UTF-8", 669 freeze_panes=(1, 1), 670 ) # saving as XLSX-file with freeze first row and column as headers 671 672 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
674 def DumpInstruments(self, forceUpdate: bool = True) -> str: 675 """ 676 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 677 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 678 679 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 680 681 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 682 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 683 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 684 """ 685 if self.iListDumpFile is None or not self.iListDumpFile: 686 uLogger.error("Output name of dump file must be defined!") 687 raise Exception("Filename required") 688 689 if not self.iList or forceUpdate: 690 self.iList = self.Listing() 691 692 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 693 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 694 fH.write(jsonDump) 695 696 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 697 698 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
700 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 701 """ 702 Show information about one instrument defined by json data and prints it in Markdown format. 703 704 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 705 706 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 707 :param show: if `True` then also printing information about instrument and its current price. 708 :return: multilines text in Markdown format with information about one instrument. 709 """ 710 splitLine = "| | |\n" 711 infoText = "" 712 713 if iJSON is not None and iJSON and isinstance(iJSON, dict): 714 info = [ 715 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 716 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 717 "| Parameters | Values |\n", 718 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 719 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 720 "| Full name: | {:<54} |\n".format(iJSON["name"]), 721 ] 722 723 if "sector" in iJSON.keys() and iJSON["sector"]: 724 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 725 726 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 727 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 728 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 729 ))) 730 731 info.extend([ 732 splitLine, 733 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 734 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 735 ]) 736 737 if "isin" in iJSON.keys() and iJSON["isin"]: 738 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 739 740 if "classCode" in iJSON.keys(): 741 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 742 743 info.extend([ 744 splitLine, 745 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 746 splitLine, 747 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 748 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 749 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 750 ]) 751 752 if iJSON["figi"]: 753 self.figi = iJSON["figi"] 754 iJSON = iJSON | self.RequestTradingStatus() 755 756 info.extend([ 757 splitLine, 758 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 759 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 760 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 761 ]) 762 763 info.append(splitLine) 764 765 if "type" in iJSON.keys() and iJSON["type"]: 766 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 767 768 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 769 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 770 771 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 772 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 773 774 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 775 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 776 777 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 778 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 779 780 if "focusType" in iJSON.keys() and iJSON["focusType"]: 781 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 782 783 if "assetType" in iJSON.keys() and iJSON["assetType"]: 784 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 785 786 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 787 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 788 789 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 790 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 791 792 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 793 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 794 795 if "currency" in iJSON.keys(): 796 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 797 798 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 799 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 800 801 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 802 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 803 804 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 805 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 806 807 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 808 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 809 810 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 811 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 812 813 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 814 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 815 816 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 817 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 818 819 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 820 info.append("| Perpetual bond: | Yes |\n") 821 822 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 823 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 824 825 iExt = None 826 if iJSON["type"] == "Bonds": 827 info.extend([ 828 splitLine, 829 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 830 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 831 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 832 iJSON["nominal"]["currency"], 833 )), 834 ]) 835 836 if "floatingCouponFlag" in iJSON.keys(): 837 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 838 839 if "amortizationFlag" in iJSON.keys(): 840 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 841 842 info.append(splitLine) 843 844 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 845 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 846 847 if iJSON["figi"]: 848 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 849 850 info.extend([ 851 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 852 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 853 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 854 ]) 855 856 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 857 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 858 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 859 iJSON["aciValue"]["currency"] 860 ))) 861 862 if "currentPrice" in iJSON.keys(): 863 info.append(splitLine) 864 865 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 866 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 867 868 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 869 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 870 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 871 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 872 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 873 874 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 875 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 876 877 info.extend([ 878 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 879 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 880 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 881 )), 882 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 883 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 884 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 885 )), 886 "| Changes between last deal price and last close | {:<54} |\n".format( 887 "{:.2f}%{}".format( 888 iJSON["currentPrice"]["changes"], 889 " ({}{:.2f} {})".format( 890 "+" if bondChangesDelta > 0 else "", 891 bondChangesDelta, 892 aciCurrency 893 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 894 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 895 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 896 currency 897 ), 898 ) 899 ), 900 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 901 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 902 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 903 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 904 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 905 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 906 )), 907 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 908 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 909 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 910 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 911 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 912 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 913 )), 914 ]) 915 916 if "lot" in iJSON.keys(): 917 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 918 919 if "step" in iJSON.keys() and iJSON["step"] != 0: 920 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 921 922 # Add bond payment calendar: 923 if iJSON["type"] == "Bonds": 924 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 925 info.extend(["\n", strCalendar]) 926 927 infoText += "".join(info) 928 929 if show: 930 uLogger.info("{}".format(infoText)) 931 932 else: 933 uLogger.debug("{}".format(infoText)) 934 935 if self.infoFile is not None: 936 with open(self.infoFile, "w", encoding="UTF-8") as fH: 937 fH.write(infoText) 938 939 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 940 941 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
943 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 944 """ 945 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 946 947 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 948 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 949 :return: JSON formatted data with information about instrument. 950 """ 951 tickerJSON = {} 952 if self.moreDebug: 953 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 954 955 if not self.ticker: 956 uLogger.warning("self.ticker variable is not be empty!") 957 958 else: 959 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 960 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 961 raise Exception("Instrument not allowed") 962 963 if not self.iList: 964 self.iList = self.Listing() 965 966 if self.ticker in self.iList["Shares"].keys(): 967 tickerJSON = self.iList["Shares"][self.ticker] 968 if self.moreDebug: 969 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 970 971 elif self.ticker in self.iList["Currencies"].keys(): 972 tickerJSON = self.iList["Currencies"][self.ticker] 973 if self.moreDebug: 974 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 975 976 elif self.ticker in self.iList["Bonds"].keys(): 977 tickerJSON = self.iList["Bonds"][self.ticker] 978 if self.moreDebug: 979 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 980 981 elif self.ticker in self.iList["Etfs"].keys(): 982 tickerJSON = self.iList["Etfs"][self.ticker] 983 if self.moreDebug: 984 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 985 986 elif self.ticker in self.iList["Futures"].keys(): 987 tickerJSON = self.iList["Futures"][self.ticker] 988 if self.moreDebug: 989 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 990 991 if tickerJSON: 992 self.figi = tickerJSON["figi"] 993 994 if requestPrice: 995 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 996 997 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 998 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 999 1000 else: 1001 tickerJSON["currentPrice"]["changes"] = 0 1002 1003 if show: 1004 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1005 1006 else: 1007 if show: 1008 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1009 1010 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1012 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1013 """ 1014 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1015 1016 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1017 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1018 :return: JSON formatted data with information about instrument. 1019 """ 1020 figiJSON = {} 1021 if self.moreDebug: 1022 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1023 1024 if not self.figi: 1025 uLogger.warning("self.figi variable is not be empty!") 1026 1027 else: 1028 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1029 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1030 raise Exception("Instrument not allowed") 1031 1032 if not self.iList: 1033 self.iList = self.Listing() 1034 1035 for item in self.iList["Shares"].keys(): 1036 if self.figi == self.iList["Shares"][item]["figi"]: 1037 figiJSON = self.iList["Shares"][item] 1038 1039 if self.moreDebug: 1040 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1041 1042 break 1043 1044 if not figiJSON: 1045 for item in self.iList["Currencies"].keys(): 1046 if self.figi == self.iList["Currencies"][item]["figi"]: 1047 figiJSON = self.iList["Currencies"][item] 1048 1049 if self.moreDebug: 1050 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1051 1052 break 1053 1054 if not figiJSON: 1055 for item in self.iList["Bonds"].keys(): 1056 if self.figi == self.iList["Bonds"][item]["figi"]: 1057 figiJSON = self.iList["Bonds"][item] 1058 1059 if self.moreDebug: 1060 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1061 1062 break 1063 1064 if not figiJSON: 1065 for item in self.iList["Etfs"].keys(): 1066 if self.figi == self.iList["Etfs"][item]["figi"]: 1067 figiJSON = self.iList["Etfs"][item] 1068 1069 if self.moreDebug: 1070 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1071 1072 break 1073 1074 if not figiJSON: 1075 for item in self.iList["Futures"].keys(): 1076 if self.figi == self.iList["Futures"][item]["figi"]: 1077 figiJSON = self.iList["Futures"][item] 1078 1079 if self.moreDebug: 1080 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1081 1082 break 1083 1084 if figiJSON: 1085 self.figi = figiJSON["figi"] 1086 self.ticker = figiJSON["ticker"] 1087 1088 if requestPrice: 1089 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1090 1091 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1092 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1093 1094 else: 1095 figiJSON["currentPrice"]["changes"] = 0 1096 1097 if show: 1098 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1099 1100 else: 1101 if show: 1102 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1103 1104 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1106 def GetCurrentPrices(self, show: bool = True) -> dict: 1107 """ 1108 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1109 `{"buy": [{"price": 1243.8, "quantity": 193}, 1110 {"price": 1244.0, "quantity": 168}, 1111 {"price": 1244.8, "quantity": 5}, 1112 {"price": 1245.0, "quantity": 61}, 1113 {"price": 1245.4, "quantity": 60}], 1114 "sell": [{"price": 1243.6, "quantity": 8}, 1115 {"price": 1242.6, "quantity": 10}, 1116 {"price": 1242.4, "quantity": 18}, 1117 {"price": 1242.2, "quantity": 50}, 1118 {"price": 1242.0, "quantity": 113}], 1119 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1120 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1121 - sell: list of dicts with Buyers prices, 1122 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1123 - quantity: volume value by current price in lots, 1124 - limitUp: current trade session limit price, maximum, 1125 - limitDown: current trade session limit price, minimum, 1126 - lastPrice: last deal price of the instrument, 1127 - closePrice: previous trade session close price of the instrument. 1128 1129 See also: `SearchByTicker()` and `SearchByFIGI()`. 1130 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1131 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1132 1133 :param show: if `True` then print DOM to log and console. 1134 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1135 If an error occurred then returns an empty record: 1136 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1137 """ 1138 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1139 1140 if self.depth < 1: 1141 uLogger.error("Depth of Market (DOM) must be >=1!") 1142 raise Exception("Incorrect value") 1143 1144 if not (self.ticker or self.figi): 1145 uLogger.error("self.ticker or self.figi variables must be defined!") 1146 raise Exception("Ticker or FIGI required") 1147 1148 if self.ticker and not self.figi: 1149 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1150 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1151 1152 if not self.ticker and self.figi: 1153 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1154 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1155 1156 if not self.figi: 1157 uLogger.error("FIGI is not defined!") 1158 raise Exception("Ticker or FIGI required") 1159 1160 else: 1161 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1162 1163 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1164 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1165 self.body = str({"figi": self.figi, "depth": self.depth}) 1166 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1167 1168 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1169 # list of dicts with sellers orders: 1170 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1171 1172 # list of dicts with buyers orders: 1173 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1174 1175 # max price of instrument at this time: 1176 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1177 1178 # min price of instrument at this time: 1179 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1180 1181 # last price of deal with instrument: 1182 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1183 1184 # last close price of instrument: 1185 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1186 1187 else: 1188 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1189 uLogger.debug("Server response: {}".format(pricesResponse)) 1190 1191 if show: 1192 if prices["buy"] or prices["sell"]: 1193 info = [ 1194 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1195 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1196 self.ticker, 1197 self.figi, 1198 self.depth, 1199 ), 1200 "-" * 60, "\n", 1201 " Orders of Buyers | Orders of Sellers\n", 1202 "-" * 60, "\n", 1203 " Sell prices (volumes) | Buy prices (volumes)\n", 1204 "-" * 60, "\n", 1205 ] 1206 1207 if not prices["buy"]: 1208 info.append(" | No orders!\n") 1209 sumBuy = 0 1210 1211 else: 1212 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1213 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1214 for item in maxMinSorted: 1215 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1216 1217 if not prices["sell"]: 1218 info.append("No orders! |\n") 1219 sumSell = 0 1220 1221 else: 1222 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1223 for item in prices["sell"]: 1224 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1225 1226 info.extend([ 1227 "-" * 60, "\n", 1228 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1229 "-" * 60, "\n", 1230 ]) 1231 1232 infoText = "".join(info) 1233 1234 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1235 1236 else: 1237 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1238 1239 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1241 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1242 """ 1243 This method get and show information about all available broker instruments for current user account. 1244 If `instrumentsFile` string is not empty then also save information to this file. 1245 1246 :param show: if `True` then print results to console, if `False` — print only to file. 1247 :return: multi-lines string with all available broker instruments 1248 """ 1249 if not self.iList: 1250 self.iList = self.Listing() 1251 1252 info = [ 1253 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1254 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1255 ] 1256 1257 # add instruments count by type: 1258 for iType in self.iList.keys(): 1259 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1260 1261 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1262 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1263 1264 # generating info tables with all instruments by type: 1265 for iType in self.iList.keys(): 1266 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1267 1268 for instrument in self.iList[iType].keys(): 1269 iName = self.iList[iType][instrument]["name"] # instrument's name 1270 if len(iName) > 57: 1271 iName = "{}...".format(iName[:54]) # right trim for a long string 1272 1273 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1274 self.iList[iType][instrument]["ticker"], 1275 iName, 1276 self.iList[iType][instrument]["figi"], 1277 self.iList[iType][instrument]["currency"], 1278 self.iList[iType][instrument]["lot"], 1279 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1280 )) 1281 1282 infoText = "".join(info) 1283 1284 if show: 1285 uLogger.info(infoText) 1286 1287 if self.instrumentsFile: 1288 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1289 fH.write(infoText) 1290 1291 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1292 1293 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1295 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1296 """ 1297 This method search and show information about instruments by part of its ticker, FIGI or name. 1298 If `searchResultsFile` string is not empty then also save information to this file. 1299 1300 :param pattern: string with part of ticker, FIGI or instrument's name. 1301 :param show: if `True` then print results to console, if `False` — return list of result only. 1302 :return: list of dictionaries with all found instruments. 1303 """ 1304 if not self.iList: 1305 self.iList = self.Listing() 1306 1307 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1308 compiledPattern = re.compile(pattern, re.IGNORECASE) 1309 1310 for iType in self.iList: 1311 for instrument in self.iList[iType].values(): 1312 searchResult = compiledPattern.search(" ".join( 1313 [instrument["ticker"], instrument["figi"], instrument["name"]] 1314 )) 1315 1316 if searchResult: 1317 searchResults[iType][instrument["ticker"]] = instrument 1318 1319 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1320 info = [ 1321 "# Search results\n\n", 1322 "* **Search pattern:** [{}]\n".format(pattern), 1323 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1324 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1325 ] 1326 infoShort = info[:] 1327 1328 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1329 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1330 skippedLine = "| ... | ... | ... | ... |\n" 1331 1332 if resultsLen == 0: 1333 info.append("\nNo results\n") 1334 infoShort.append("\nNo results\n") 1335 uLogger.warning("No results. Try changing your search pattern.") 1336 1337 else: 1338 for iType in searchResults: 1339 iTypeValuesCount = len(searchResults[iType].values()) 1340 if iTypeValuesCount > 0: 1341 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1342 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1343 1344 for instrument in searchResults[iType].values(): 1345 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1346 instrument["type"], 1347 instrument["ticker"], 1348 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1349 instrument["figi"], 1350 )) 1351 1352 if iTypeValuesCount <= 5: 1353 infoShort.extend(info[-iTypeValuesCount:]) 1354 1355 else: 1356 infoShort.extend(info[-5:]) 1357 infoShort.append(skippedLine) 1358 1359 infoText = "".join(info) 1360 infoTextShort = "".join(infoShort) 1361 1362 if show: 1363 uLogger.info(infoTextShort) 1364 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1365 1366 if self.searchResultsFile: 1367 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1368 fH.write(infoText) 1369 1370 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1371 1372 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1374 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1375 """ 1376 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1377 1378 :param instruments: list of strings with tickers or FIGIs. 1379 :return: list with unique instrument FIGIs only. 1380 """ 1381 requestedInstruments = [] 1382 for iName in instruments: 1383 if iName not in self.aliases.keys(): 1384 if iName not in requestedInstruments: 1385 requestedInstruments.append(iName) 1386 1387 else: 1388 if iName not in requestedInstruments: 1389 if self.aliases[iName] not in requestedInstruments: 1390 requestedInstruments.append(self.aliases[iName]) 1391 1392 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1393 1394 onlyUniqueFIGIs = [] 1395 for iName in requestedInstruments: 1396 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1397 continue 1398 1399 self.ticker = iName 1400 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1401 1402 if not iData: 1403 self.ticker = "" 1404 self.figi = iName 1405 1406 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1407 1408 if not iData: 1409 self.figi = "" 1410 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1411 1412 if iData and iData["figi"] not in onlyUniqueFIGIs: 1413 onlyUniqueFIGIs.append(iData["figi"]) 1414 1415 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1416 1417 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1419 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1420 """ 1421 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1422 1423 See limits: https://tinkoff.github.io/investAPI/limits/ 1424 1425 If `pricesFile` string is not empty then also save information to this file. 1426 1427 :param instruments: list of strings with tickers or FIGIs. 1428 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1429 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1430 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1431 """ 1432 if instruments is None or not instruments: 1433 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1434 raise Exception("Ticker or FIGI required") 1435 1436 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1437 1438 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1439 1440 iList = [] # trying to get info and current prices about all unique instruments: 1441 for self.figi in onlyUniqueFIGIs: 1442 iData = self.SearchByFIGI(requestPrice=True) 1443 iList.append(iData) 1444 1445 self.ShowListOfPrices(iList, show) 1446 1447 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1449 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1450 """ 1451 Show table contains current prices of given instruments. 1452 1453 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1454 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1455 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1456 :return: multilines text in Markdown format as a table contains current prices. 1457 """ 1458 infoText = "" 1459 1460 if show or self.pricesFile: 1461 info = [ 1462 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1463 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1464 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1465 ] 1466 1467 for item in iList: 1468 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1469 item["ticker"], 1470 item["figi"], 1471 item["type"], 1472 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1473 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1474 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1475 "{} / {}".format( 1476 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1477 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1478 ), 1479 "{} / {}".format( 1480 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1481 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1482 ), 1483 item["currency"], 1484 )) 1485 1486 infoText = "".join(info) 1487 1488 if show: 1489 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1490 1491 if self.pricesFile: 1492 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1493 fH.write(infoText) 1494 1495 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1496 1497 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1499 def RequestTradingStatus(self) -> dict: 1500 """ 1501 Requesting trading status for the instrument defined by `figi` variable. 1502 1503 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1504 1505 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1506 1507 :return: dictionary with trading status attributes. Response example: 1508 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1509 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1510 """ 1511 if self.figi is None or not self.figi: 1512 uLogger.error("Variable `figi` must be defined for using this method!") 1513 raise Exception("FIGI required") 1514 1515 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1516 1517 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1518 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1519 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1520 1521 if self.moreDebug: 1522 uLogger.debug("Records about current trading status successfully received") 1523 1524 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1526 def RequestPortfolio(self) -> dict: 1527 """ 1528 Requesting actual user's portfolio for current `accountId`. 1529 1530 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1531 1532 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1533 1534 :return: dictionary with user's portfolio. 1535 """ 1536 if self.accountId is None or not self.accountId: 1537 uLogger.error("Variable `accountId` must be defined for using this method!") 1538 raise Exception("Account ID required") 1539 1540 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1541 1542 self.body = str({"accountId": self.accountId}) 1543 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1544 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1545 1546 if self.moreDebug: 1547 uLogger.debug("Records about user's portfolio successfully received") 1548 1549 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1551 def RequestPositions(self) -> dict: 1552 """ 1553 Requesting open positions by currencies and instruments for current `accountId`. 1554 1555 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1556 1557 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1558 1559 :return: dictionary with open positions by instruments. 1560 """ 1561 if self.accountId is None or not self.accountId: 1562 uLogger.error("Variable `accountId` must be defined for using this method!") 1563 raise Exception("Account ID required") 1564 1565 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1566 1567 self.body = str({"accountId": self.accountId}) 1568 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1569 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1570 1571 if self.moreDebug: 1572 uLogger.debug("Records about current open positions successfully received") 1573 1574 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1576 def RequestPendingOrders(self) -> list: 1577 """ 1578 Requesting current actual pending orders for current `accountId`. 1579 1580 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1581 1582 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1583 1584 :return: list of dictionaries with pending orders. 1585 """ 1586 if self.accountId is None or not self.accountId: 1587 uLogger.error("Variable `accountId` must be defined for using this method!") 1588 raise Exception("Account ID required") 1589 1590 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1591 1592 self.body = str({"accountId": self.accountId}) 1593 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1594 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1595 1596 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1597 1598 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1600 def RequestStopOrders(self) -> list: 1601 """ 1602 Requesting current actual stop orders for current `accountId`. 1603 1604 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1605 1606 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1607 1608 :return: list of dictionaries with stop orders. 1609 """ 1610 if self.accountId is None or not self.accountId: 1611 uLogger.error("Variable `accountId` must be defined for using this method!") 1612 raise Exception("Account ID required") 1613 1614 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1615 1616 self.body = str({"accountId": self.accountId}) 1617 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1618 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1619 1620 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1621 1622 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1624 def Overview(self, show: bool = False, details: str = "full") -> dict: 1625 """ 1626 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1627 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1628 and `overviewBondsCalendarFile` are defined then also save information to file. 1629 1630 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1631 many requests about the state of the portfolio, and then, based on the received data, a large number 1632 of calculation and statistics are collected. 1633 1634 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1635 :param details: how detailed should the information be? 1636 - `full` — shows full available information about portfolio status (by default), 1637 - `positions` — shows only open positions, 1638 - `orders` — shows only sections of open limits and stop orders. 1639 - `digest` — show a short digest of the portfolio status, 1640 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1641 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1642 :return: dictionary with client's raw portfolio and some statistics. 1643 """ 1644 if self.accountId is None or not self.accountId: 1645 uLogger.error("Variable `accountId` must be defined for using this method!") 1646 raise Exception("Account ID required") 1647 1648 view = { 1649 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1650 "headers": {}, # list of dictionaries, response headers without "positions" section 1651 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1652 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1653 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1654 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1655 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1656 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1657 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1658 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1659 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1660 }, 1661 "stat": { # --- some statistics calculated using "raw" sections: 1662 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1663 "availableRUB": 0., # available rubles (without other currencies) 1664 "blockedRUB": 0., # blocked sum in Russian Rouble 1665 "totalChangesRUB": 0., # changes for all open trades in RUB 1666 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1667 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1668 "sharesCostRUB": 0., # costs of all shares in RUB 1669 "bondsCostRUB": 0., # costs of all bonds in RUB 1670 "etfsCostRUB": 0., # costs of all etfs in RUB 1671 "futuresCostRUB": 0., # costs of all futures in RUB 1672 "Currencies": [], # list of dictionaries of all currencies statistics 1673 "Shares": [], # list of dictionaries of all shares statistics 1674 "Bonds": [], # list of dictionaries of all bonds statistics 1675 "Etfs": [], # list of dictionaries of all etfs statistics 1676 "Futures": [], # list of dictionaries of all futures statistics 1677 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1678 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1679 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1680 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1681 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1682 }, 1683 "analytics": { # --- some analytics of portfolio: 1684 "distrByAssets": {}, # portfolio distribution by assets 1685 "distrByCompanies": {}, # portfolio distribution by companies 1686 "distrBySectors": {}, # portfolio distribution by sectors 1687 "distrByCurrencies": {}, # portfolio distribution by currencies 1688 "distrByCountries": {}, # portfolio distribution by countries 1689 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1690 } 1691 } 1692 1693 details = details.lower() 1694 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1695 if details not in availableDetails: 1696 details = "full" 1697 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1698 1699 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1700 1701 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1702 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1703 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1704 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1705 1706 # save response headers without "positions" section: 1707 for key in portfolioResponse.keys(): 1708 if key != "positions": 1709 view["raw"]["headers"][key] = portfolioResponse[key] 1710 1711 else: 1712 continue 1713 1714 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1715 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1716 for item in portfolioResponse["positions"]: 1717 if item["instrumentType"] == "currency": 1718 self.figi = item["figi"] 1719 curr = self.SearchByFIGI(requestPrice=False) 1720 1721 # current price of currency in RUB: 1722 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1723 "name": curr["name"], 1724 "currentPrice": NanoToFloat( 1725 item["currentPrice"]["units"], 1726 item["currentPrice"]["nano"] 1727 ), 1728 } 1729 1730 view["raw"]["Currencies"].append(item) 1731 1732 elif item["instrumentType"] == "share": 1733 view["raw"]["Shares"].append(item) 1734 1735 elif item["instrumentType"] == "bond": 1736 view["raw"]["Bonds"].append(item) 1737 1738 elif item["instrumentType"] == "etf": 1739 view["raw"]["Etfs"].append(item) 1740 1741 elif item["instrumentType"] == "futures": 1742 view["raw"]["Futures"].append(item) 1743 1744 else: 1745 continue 1746 1747 # how many volume of currencies (by ISO currency name) are blocked: 1748 for item in view["raw"]["positions"]["blocked"]: 1749 blocked = NanoToFloat(item["units"], item["nano"]) 1750 if blocked > 0: 1751 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1752 1753 # how many volume of instruments (by FIGI) are blocked: 1754 for item in view["raw"]["positions"]["securities"]: 1755 blocked = int(item["blocked"]) 1756 if blocked > 0: 1757 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1758 1759 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1760 1761 if "rub" in allBlocked.keys(): 1762 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1763 1764 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1765 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1766 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1767 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1768 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1769 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1770 view["stat"]["portfolioCostRUB"] = sum([ 1771 view["stat"]["allCurrenciesCostRUB"], 1772 view["stat"]["sharesCostRUB"], 1773 view["stat"]["bondsCostRUB"], 1774 view["stat"]["etfsCostRUB"], 1775 view["stat"]["futuresCostRUB"], 1776 ]) 1777 1778 # --- calculating some portfolio statistics: 1779 byComp = {} # distribution by companies 1780 bySect = {} # distribution by sectors 1781 byCurr = {} # distribution by currencies (include RUB) 1782 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1783 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1784 1785 for item in portfolioResponse["positions"]: 1786 self.figi = item["figi"] 1787 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1788 1789 if instrument: 1790 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1791 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1792 1793 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1794 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1795 1796 else: 1797 blocked = 0 1798 1799 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1800 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1801 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1802 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1803 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1804 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1805 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1806 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1807 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1808 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1809 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1810 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1811 1812 statData = { 1813 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1814 "ticker": instrument["ticker"], # ticker by FIGI 1815 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1816 "volume": volume, # available volume of instrument 1817 "lots": lots, # volume in lots of instrument 1818 "direction": direction, # direction of an instrument's position: short or long 1819 "blocked": blocked, # blocked volume of currency or instrument 1820 "currentPrice": curPrice, # current instrument's price in basic asset 1821 "average": average, # current average position price 1822 "cost": cost, # current cost of all volume of instrument in basic asset 1823 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1824 "costRUB": costRUB, # cost of instrument in ruble 1825 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1826 "profit": profit, # expected profit at current moment 1827 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1828 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1829 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1830 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1831 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1832 "step": instrument["step"], # minimum price increment 1833 } 1834 1835 # adding distribution by unique countries: 1836 if statData["country"] not in byCountry.keys(): 1837 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1838 1839 else: 1840 byCountry[statData["country"]]["cost"] += costRUB 1841 byCountry[statData["country"]]["percent"] += percentCostRUB 1842 1843 if item["instrumentType"] != "currency": 1844 # adding distribution by unique companies: 1845 if statData["name"]: 1846 if statData["name"] not in byComp.keys(): 1847 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1848 1849 else: 1850 byComp[statData["name"]]["cost"] += costRUB 1851 byComp[statData["name"]]["percent"] += percentCostRUB 1852 1853 # adding distribution by unique sectors: 1854 if statData["sector"] not in bySect.keys(): 1855 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1856 1857 else: 1858 bySect[statData["sector"]]["cost"] += costRUB 1859 bySect[statData["sector"]]["percent"] += percentCostRUB 1860 1861 # adding distribution by unique currencies: 1862 if currency not in byCurr.keys(): 1863 byCurr[currency] = { 1864 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1865 "cost": costRUB, 1866 "percent": percentCostRUB 1867 } 1868 1869 else: 1870 byCurr[currency]["cost"] += costRUB 1871 byCurr[currency]["percent"] += percentCostRUB 1872 1873 # saving statistics for every instrument: 1874 if item["instrumentType"] == "currency": 1875 view["stat"]["Currencies"].append(statData) 1876 1877 # update dict with free funds for trading (total - blocked) by currencies 1878 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1879 view["stat"]["funds"][currency] = { 1880 "total": volume, 1881 "totalCostRUB": costRUB, # total volume cost in rubles 1882 "free": volume - blocked, 1883 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1884 } 1885 1886 elif item["instrumentType"] == "share": 1887 view["stat"]["Shares"].append(statData) 1888 1889 elif item["instrumentType"] == "bond": 1890 view["stat"]["Bonds"].append(statData) 1891 1892 elif item["instrumentType"] == "etf": 1893 view["stat"]["Etfs"].append(statData) 1894 1895 elif item["instrumentType"] == "Futures": 1896 view["stat"]["Futures"].append(statData) 1897 1898 else: 1899 continue 1900 1901 # total changes in Russian Ruble: 1902 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1903 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1904 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1905 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1906 view["stat"]["funds"]["rub"] = { 1907 "total": view["stat"]["availableRUB"], 1908 "totalCostRUB": view["stat"]["availableRUB"], 1909 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1910 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1911 } 1912 1913 # --- pending orders sector data: 1914 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1915 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1916 1917 for item in view["raw"]["orders"]: 1918 self.figi = item["figi"] 1919 1920 if item["figi"] not in uniquePendingOrdersFIGIs: 1921 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1922 1923 uniquePendingOrdersFIGIs.append(item["figi"]) 1924 uniquePendingOrders[item["figi"]] = instrument 1925 1926 else: 1927 instrument = uniquePendingOrders[item["figi"]] 1928 1929 if instrument: 1930 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1931 orderType = TKS_ORDER_TYPES[item["orderType"]] 1932 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1933 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1934 1935 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1936 if item["direction"] == "ORDER_DIRECTION_BUY": 1937 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1938 1939 else: 1940 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1941 1942 # requested price for order execution: 1943 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1944 1945 # necessary changes in percent to reach target from current price: 1946 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1947 1948 view["stat"]["orders"].append({ 1949 "orderID": item["orderId"], # orderId number parameter of current order 1950 "figi": item["figi"], # FIGI identification 1951 "ticker": instrument["ticker"], # ticker name by FIGI 1952 "lotsRequested": item["lotsRequested"], # requested lots value 1953 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1954 "currentPrice": lastPrice, # current instrument's price for defined action 1955 "targetPrice": target, # requested price for order execution in base currency 1956 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1957 "percentChanges": changes, # changes in percent to target from current price 1958 "currency": item["currency"], # instrument's currency name 1959 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1960 "type": orderType, # type of order from TKS_ORDER_TYPES 1961 "status": orderState, # order status from TKS_ORDER_STATES 1962 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1963 }) 1964 1965 # --- stop orders sector data: 1966 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1967 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1968 1969 for item in view["raw"]["stopOrders"]: 1970 self.figi = item["figi"] 1971 1972 if item["figi"] not in uniqueStopOrdersFIGIs: 1973 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1974 1975 uniqueStopOrdersFIGIs.append(item["figi"]) 1976 uniqueStopOrders[item["figi"]] = instrument 1977 1978 else: 1979 instrument = uniqueStopOrders[item["figi"]] 1980 1981 if instrument: 1982 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1983 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1984 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1985 1986 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1987 if "expirationTime" in item.keys(): 1988 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1989 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1990 1991 else: 1992 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1993 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1994 1995 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1996 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1997 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1998 1999 else: 2000 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2001 2002 # requested price when stop-order executed: 2003 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2004 2005 # price for limit-order, set up when stop-order executed: 2006 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2007 2008 # necessary changes in percent to reach target from current price: 2009 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2010 2011 view["stat"]["stopOrders"].append({ 2012 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2013 "figi": item["figi"], # FIGI identification 2014 "ticker": instrument["ticker"], # ticker name by FIGI 2015 "lotsRequested": item["lotsRequested"], # requested lots value 2016 "currentPrice": lastPrice, # current instrument's price for defined action 2017 "targetPrice": target, # requested price for stop-order execution in base currency 2018 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2019 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2020 "percentChanges": changes, # changes in percent to target from current price 2021 "currency": item["currency"], # instrument's currency name 2022 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2023 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2024 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2025 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2026 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2027 }) 2028 2029 # --- calculating data for analytics section: 2030 # portfolio distribution by assets: 2031 view["analytics"]["distrByAssets"] = { 2032 "Ruble": { 2033 "uniques": 1, 2034 "cost": view["stat"]["availableRUB"], 2035 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2036 }, 2037 "Currencies": { 2038 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2039 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2040 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2041 }, 2042 "Shares": { 2043 "uniques": len(view["stat"]["Shares"]), 2044 "cost": view["stat"]["sharesCostRUB"], 2045 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2046 }, 2047 "Bonds": { 2048 "uniques": len(view["stat"]["Bonds"]), 2049 "cost": view["stat"]["bondsCostRUB"], 2050 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2051 }, 2052 "Etfs": { 2053 "uniques": len(view["stat"]["Etfs"]), 2054 "cost": view["stat"]["etfsCostRUB"], 2055 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2056 }, 2057 "Futures": { 2058 "uniques": len(view["stat"]["Futures"]), 2059 "cost": view["stat"]["futuresCostRUB"], 2060 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2061 }, 2062 } 2063 2064 # portfolio distribution by companies: 2065 view["analytics"]["distrByCompanies"]["All money cash"] = { 2066 "ticker": "", 2067 "cost": view["stat"]["allCurrenciesCostRUB"], 2068 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2069 } 2070 view["analytics"]["distrByCompanies"].update(byComp) 2071 2072 # portfolio distribution by sectors: 2073 view["analytics"]["distrBySectors"]["All money cash"] = { 2074 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2075 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2076 } 2077 view["analytics"]["distrBySectors"].update(bySect) 2078 2079 # portfolio distribution by currencies: 2080 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2081 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2082 2083 if self.moreDebug: 2084 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2085 2086 view["analytics"]["distrByCurrencies"].update(byCurr) 2087 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2088 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2089 2090 # portfolio distribution by countries: 2091 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2092 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2093 2094 if self.moreDebug: 2095 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2096 2097 view["analytics"]["distrByCountries"].update(byCountry) 2098 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2099 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2100 2101 # --- Prepare text statistics overview in human-readable: 2102 if show: 2103 # Whatever the value `details`, header not changes: 2104 info = [ 2105 "# Client's portfolio\n\n", 2106 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2107 "* **Account ID:** [{}]\n".format(self.accountId), 2108 ] 2109 2110 if details in ["full", "positions", "digest"]: 2111 info.extend([ 2112 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2113 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2114 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2115 view["stat"]["totalChangesRUB"], 2116 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2117 view["stat"]["totalChangesPercentRUB"], 2118 ), 2119 ]) 2120 2121 if details in ["full", "positions"]: 2122 info.extend([ 2123 "## Open positions\n\n", 2124 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2125 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2126 "| Ruble | {:>31} | | | | | |\n".format( 2127 "{:.2f} ({:.2f}) rub".format( 2128 view["stat"]["availableRUB"], 2129 view["stat"]["blockedRUB"], 2130 ) 2131 ) 2132 ]) 2133 2134 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2135 return [ 2136 "| | | | | | | |\n", 2137 "| {:<27} | | | | | {:>19} | |\n".format( 2138 noTradeStr if noTradeStr else typeStr, 2139 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2140 ), 2141 ] 2142 2143 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2144 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2145 "{} [{}]".format(data["ticker"], data["figi"]), 2146 "{:.2f} ({:.2f}) {}".format( 2147 data["volume"], 2148 data["blocked"], 2149 data["currency"], 2150 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2151 data["volume"], 2152 data["blocked"], 2153 ), 2154 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2155 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2156 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2157 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2158 "{}{:.2f} {} ({}{:.2f}%)".format( 2159 "+" if data["profit"] > 0 else "", 2160 data["profit"], data["baseCurrencyName"], 2161 "+" if data["percentProfit"] > 0 else "", 2162 data["percentProfit"], 2163 ), 2164 ) 2165 2166 # --- Show currencies section: 2167 if view["stat"]["Currencies"]: 2168 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2169 for item in view["stat"]["Currencies"]: 2170 info.append(_InfoStr(item, showCurrencyName=True)) 2171 2172 else: 2173 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2174 2175 # --- Show shares section: 2176 if view["stat"]["Shares"]: 2177 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2178 2179 for item in view["stat"]["Shares"]: 2180 info.append(_InfoStr(item)) 2181 2182 else: 2183 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2184 2185 # --- Show bonds section: 2186 if view["stat"]["Bonds"]: 2187 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2188 2189 for item in view["stat"]["Bonds"]: 2190 info.append(_InfoStr(item)) 2191 2192 else: 2193 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2194 2195 # --- Show etfs section: 2196 if view["stat"]["Etfs"]: 2197 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2198 2199 for item in view["stat"]["Etfs"]: 2200 info.append(_InfoStr(item)) 2201 2202 else: 2203 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2204 2205 # --- Show futures section: 2206 if view["stat"]["Futures"]: 2207 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2208 2209 for item in view["stat"]["Futures"]: 2210 info.append(_InfoStr(item)) 2211 2212 else: 2213 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2214 2215 if details in ["full", "orders"]: 2216 # --- Show pending orders section: 2217 if view["stat"]["orders"]: 2218 info.extend([ 2219 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2220 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2221 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2222 ]) 2223 2224 for item in view["stat"]["orders"]: 2225 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2226 "{} [{}]".format(item["ticker"], item["figi"]), 2227 item["orderID"], 2228 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2229 "{} {} ({}{:.2f}%)".format( 2230 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2231 item["baseCurrencyName"], 2232 "+" if item["percentChanges"] > 0 else "", 2233 float(item["percentChanges"]), 2234 ), 2235 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2236 item["action"], 2237 item["type"], 2238 item["date"], 2239 )) 2240 2241 else: 2242 info.append("\n## Total pending limit-orders: 0\n") 2243 2244 # --- Show stop orders section: 2245 if view["stat"]["stopOrders"]: 2246 info.extend([ 2247 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2248 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2249 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2250 ]) 2251 2252 for item in view["stat"]["stopOrders"]: 2253 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2254 "{} [{}]".format(item["ticker"], item["figi"]), 2255 item["orderID"], 2256 item["lotsRequested"], 2257 "{} {} ({}{:.2f}%)".format( 2258 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2259 item["baseCurrencyName"], 2260 "+" if item["percentChanges"] > 0 else "", 2261 float(item["percentChanges"]), 2262 ), 2263 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2264 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2265 item["action"], 2266 item["type"], 2267 item["expType"], 2268 item["createDate"], 2269 item["expDate"], 2270 )) 2271 2272 else: 2273 info.append("\n## Total stop-orders: 0\n") 2274 2275 if details in ["full", "analytics"]: 2276 # -- Show analytics section: 2277 if view["stat"]["portfolioCostRUB"] > 0: 2278 info.extend([ 2279 "\n# Analytics\n" 2280 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2281 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2282 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2283 view["stat"]["totalChangesRUB"], 2284 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2285 view["stat"]["totalChangesPercentRUB"], 2286 ), 2287 "\n## Portfolio distribution by assets\n" 2288 "\n| Type | Uniques | Percent | Current cost |\n", 2289 "|------------------------------------|---------|---------|--------------------|\n", 2290 ]) 2291 2292 for key in view["analytics"]["distrByAssets"].keys(): 2293 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2294 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2295 key, 2296 view["analytics"]["distrByAssets"][key]["uniques"], 2297 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2298 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2299 )) 2300 2301 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2302 2303 info.extend([ 2304 "\n## Portfolio distribution by companies\n" 2305 "\n| Company | Percent | Current cost |\n", 2306 aSepLine, 2307 ]) 2308 2309 for company in view["analytics"]["distrByCompanies"].keys(): 2310 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2311 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2312 "{}{}".format( 2313 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2314 company, 2315 ), 2316 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2317 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2318 )) 2319 2320 info.extend([ 2321 "\n## Portfolio distribution by sectors\n" 2322 "\n| Sector | Percent | Current cost |\n", 2323 aSepLine, 2324 ]) 2325 2326 for sector in view["analytics"]["distrBySectors"].keys(): 2327 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2328 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2329 sector, 2330 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2331 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2332 )) 2333 2334 info.extend([ 2335 "\n## Portfolio distribution by currencies\n" 2336 "\n| Instruments currencies | Percent | Current cost |\n", 2337 aSepLine, 2338 ]) 2339 2340 for curr in view["analytics"]["distrByCurrencies"].keys(): 2341 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2342 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2343 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2344 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2345 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2346 )) 2347 2348 info.extend([ 2349 "\n## Portfolio distribution by countries\n" 2350 "\n| Assets by country | Percent | Current cost |\n", 2351 aSepLine, 2352 ]) 2353 2354 for country in view["analytics"]["distrByCountries"].keys(): 2355 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2356 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2357 country, 2358 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2359 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2360 )) 2361 2362 if details in ["full", "calendar"]: 2363 # -- Show bonds payment calendar section: 2364 if view["stat"]["Bonds"]: 2365 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2366 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2367 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2368 2369 else: 2370 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2371 2372 infoText = "".join(info) 2373 2374 uLogger.info(infoText) 2375 2376 if details == "full" and self.overviewFile: 2377 filename = self.overviewFile 2378 2379 elif details == "digest" and self.overviewDigestFile: 2380 filename = self.overviewDigestFile 2381 2382 elif details == "positions" and self.overviewPositionsFile: 2383 filename = self.overviewPositionsFile 2384 2385 elif details == "orders" and self.overviewOrdersFile: 2386 filename = self.overviewOrdersFile 2387 2388 elif details == "analytics" and self.overviewAnalyticsFile: 2389 filename = self.overviewAnalyticsFile 2390 2391 elif details == "calendar" and self.overviewBondsCalendarFile: 2392 filename = self.overviewBondsCalendarFile 2393 2394 else: 2395 filename = "" 2396 2397 if filename: 2398 with open(filename, "w", encoding="UTF-8") as fH: 2399 fH.write(infoText) 2400 2401 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2402 2403 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2405 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2406 """ 2407 Returns history operations between two given dates for current `accountId`. 2408 If `reportFile` string is not empty then also save human-readable report. 2409 Shows some statistical data of closed positions. 2410 2411 :param start: see docstring in `GetDatesAsString()` method 2412 :param end: see docstring in `GetDatesAsString()` method 2413 :param show: if `True` then also prints all records to the console. 2414 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2415 :return: original list of dictionaries with history of deals records from API ("operations" key): 2416 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2417 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2418 """ 2419 if self.accountId is None or not self.accountId: 2420 uLogger.error("Variable `accountId` must be defined for using this method!") 2421 raise Exception("Account ID required") 2422 2423 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2424 2425 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2426 2427 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2428 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2429 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2430 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2431 customStat = {} # custom statistics in additional to responseJSON 2432 2433 # --- output report in human-readable format: 2434 if show or self.reportFile: 2435 splitLine1 = "| | | | | |\n" # Summary section 2436 splitLine2 = "| | | | | | | | |\n" # Operations section 2437 nextDay = "" 2438 2439 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2440 2441 if len(ops) > 0: 2442 customStat = { 2443 "opsCount": 0, # total operations count 2444 "buyCount": 0, # buy operations 2445 "sellCount": 0, # sell operations 2446 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2447 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2448 "payIn": {"rub": 0.}, # Deposit brokerage account 2449 "payOut": {"rub": 0.}, # Withdrawals 2450 "divs": {"rub": 0.}, # Dividends income 2451 "coupons": {"rub": 0.}, # Coupon's income 2452 "brokerCom": {"rub": 0.}, # Service commissions 2453 "serviceCom": {"rub": 0.}, # Service commissions 2454 "marginCom": {"rub": 0.}, # Margin commissions 2455 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2456 } 2457 2458 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2459 for item in ops: 2460 if item["state"] == "OPERATION_STATE_EXECUTED": 2461 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2462 2463 # count buy operations: 2464 if "_BUY" in item["operationType"]: 2465 customStat["buyCount"] += 1 2466 2467 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2468 customStat["buyTotal"][item["payment"]["currency"]] += payment 2469 2470 else: 2471 customStat["buyTotal"][item["payment"]["currency"]] = payment 2472 2473 # count sell operations: 2474 elif "_SELL" in item["operationType"]: 2475 customStat["sellCount"] += 1 2476 2477 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2478 customStat["sellTotal"][item["payment"]["currency"]] += payment 2479 2480 else: 2481 customStat["sellTotal"][item["payment"]["currency"]] = payment 2482 2483 # count incoming operations: 2484 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2485 if item["payment"]["currency"] in customStat["payIn"].keys(): 2486 customStat["payIn"][item["payment"]["currency"]] += payment 2487 2488 else: 2489 customStat["payIn"][item["payment"]["currency"]] = payment 2490 2491 # count withdrawals operations: 2492 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2493 if item["payment"]["currency"] in customStat["payOut"].keys(): 2494 customStat["payOut"][item["payment"]["currency"]] += payment 2495 2496 else: 2497 customStat["payOut"][item["payment"]["currency"]] = payment 2498 2499 # count dividends income: 2500 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2501 if item["payment"]["currency"] in customStat["divs"].keys(): 2502 customStat["divs"][item["payment"]["currency"]] += payment 2503 2504 else: 2505 customStat["divs"][item["payment"]["currency"]] = payment 2506 2507 # count coupon's income: 2508 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2509 if item["payment"]["currency"] in customStat["coupons"].keys(): 2510 customStat["coupons"][item["payment"]["currency"]] += payment 2511 2512 else: 2513 customStat["coupons"][item["payment"]["currency"]] = payment 2514 2515 # count broker commissions: 2516 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2517 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2518 customStat["brokerCom"][item["payment"]["currency"]] += payment 2519 2520 else: 2521 customStat["brokerCom"][item["payment"]["currency"]] = payment 2522 2523 # count service commissions: 2524 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2525 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2526 customStat["serviceCom"][item["payment"]["currency"]] += payment 2527 2528 else: 2529 customStat["serviceCom"][item["payment"]["currency"]] = payment 2530 2531 # count margin commissions: 2532 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2533 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2534 customStat["marginCom"][item["payment"]["currency"]] += payment 2535 2536 else: 2537 customStat["marginCom"][item["payment"]["currency"]] = payment 2538 2539 # count withholding taxes: 2540 elif "_TAX" in item["operationType"]: 2541 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2542 customStat["allTaxes"][item["payment"]["currency"]] += payment 2543 2544 else: 2545 customStat["allTaxes"][item["payment"]["currency"]] = payment 2546 2547 else: 2548 continue 2549 2550 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2551 2552 # --- view "Actions" lines: 2553 info.extend([ 2554 "| Report sections | | | | |\n", 2555 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2556 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2557 "| | Buy: {:<22} | {:<28} | | |\n".format( 2558 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2559 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2560 ), 2561 "| | Sell: {:<21} | {:<28} | | |\n".format( 2562 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2563 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2564 ), 2565 ]) 2566 2567 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2568 for key in opsKeys: 2569 if key == "rub": 2570 continue 2571 2572 info.extend([ 2573 "| | | {:<28} | | |\n".format( 2574 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2575 ), 2576 "| | | {:<28} | | |\n".format( 2577 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2578 ), 2579 ]) 2580 2581 info.append(splitLine1) 2582 2583 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2584 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2585 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2586 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2587 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2588 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2589 ) 2590 2591 # --- view "Payments" lines: 2592 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2593 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2594 2595 for key in paymentsKeys: 2596 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2597 2598 info.append(splitLine1) 2599 2600 # --- view "Commissions and taxes" lines: 2601 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2602 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2603 2604 for key in comKeys: 2605 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2606 2607 info.append(splitLine1) 2608 2609 info.extend([ 2610 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2611 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2612 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2613 ]) 2614 2615 else: 2616 info.append("Broker returned no operations during this period\n") 2617 2618 # --- view "Operations" section: 2619 for item in ops: 2620 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2621 continue 2622 2623 else: 2624 self.figi = item["figi"] if item["figi"] else "" 2625 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2626 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2627 2628 # group of deals during one day: 2629 if nextDay and item["date"].split("T")[0] != nextDay: 2630 info.append(splitLine2) 2631 nextDay = "" 2632 2633 else: 2634 nextDay = item["date"].split("T")[0] # saving current day for splitting 2635 2636 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2637 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2638 self.figi if self.figi else "—", 2639 instrument["ticker"] if instrument else "—", 2640 instrument["type"] if instrument else "—", 2641 item["quantity"] if int(item["quantity"]) > 0 else "—", 2642 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2643 TKS_OPERATION_STATES[item["state"]], 2644 TKS_OPERATION_TYPES[item["operationType"]], 2645 )) 2646 2647 infoText = "".join(info) 2648 2649 if show: 2650 if self.moreDebug: 2651 uLogger.debug("Records about history of a client's operations successfully received") 2652 2653 uLogger.info(infoText) 2654 2655 if self.reportFile: 2656 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2657 fH.write(infoText) 2658 2659 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2660 2661 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2663 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2664 """ 2665 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2666 2667 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2668 Warning! Broker server used ISO UTC time by default. 2669 2670 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2671 Also, `historyFile` used to update history with `onlyMissing` parameter. 2672 2673 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2674 2675 :param start: see docstring in `GetDatesAsString()` method. 2676 :param end: see docstring in `GetDatesAsString()` method. 2677 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2678 `"hour"`, `"day"`. Default: `"hour"`. 2679 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2680 False by default. Warning! History appends only from last candle to current time 2681 with always update last candle! 2682 :param csvSep: separator if csv-file is used, `,` by default. 2683 :param show: if `True` then also prints Pandas DataFrame to the console. 2684 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2685 `["date", "time", "open", "high", "low", "close", "volume"]`. 2686 """ 2687 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2688 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2689 history = None # empty pandas object for history 2690 2691 if interval not in TKS_CANDLE_INTERVALS.keys(): 2692 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2693 raise Exception("Incorrect value") 2694 2695 if not (self.ticker or self.figi): 2696 uLogger.error("Ticker or FIGI must be defined!") 2697 raise Exception("Ticker or FIGI required") 2698 2699 if self.ticker and not self.figi: 2700 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2701 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2702 2703 if self.figi and not self.ticker: 2704 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2705 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2706 2707 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2708 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2709 if interval.lower() != "day": 2710 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2711 2712 delta = dtEnd - dtStart # current UTC time minus last time in file 2713 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2714 2715 # calculate history length in candles: 2716 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2717 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2718 length += 1 # to avoid fraction time 2719 2720 # calculate data blocks count: 2721 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2722 2723 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2724 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2725 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2726 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2727 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2728 2729 tempOld = None # pandas object for old history, if --only-missing key present 2730 lastTime = None # datetime object of last old candle in file 2731 2732 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2733 uLogger.debug("--only-missing key present, add only last missing candles...") 2734 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2735 2736 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2737 2738 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2739 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2740 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2741 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2742 2743 # get last datetime object from last string in file or minus 1 delta if file is empty: 2744 if len(tempOld) > 0: 2745 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2746 2747 else: 2748 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2749 2750 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2751 2752 responseJSONs = [] # raw history blocks of data 2753 2754 blockEnd = dtEnd 2755 for item in range(blocks): 2756 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2757 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2758 2759 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2760 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2761 )) 2762 2763 if blockStart == blockEnd: 2764 uLogger.debug("Skipped this zero-length block...") 2765 2766 else: 2767 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2768 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2769 self.body = str({ 2770 "figi": self.figi, 2771 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2772 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2773 "interval": TKS_CANDLE_INTERVALS[interval][0] 2774 }) 2775 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2776 2777 if "code" in responseJSON.keys(): 2778 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2779 2780 else: 2781 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2782 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2783 2784 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2785 2786 blockEnd = blockStart 2787 2788 printCount = len(responseJSONs) # candles to show in console 2789 if responseJSONs: 2790 tempHistory = pd.DataFrame( 2791 data={ 2792 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2793 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2794 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2795 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2796 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2797 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2798 "volume": [int(item["volume"]) for item in responseJSONs], 2799 }, 2800 index=range(len(responseJSONs)), 2801 columns=["date", "time", "open", "high", "low", "close", "volume"], 2802 ) 2803 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2804 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2805 2806 # append only newest candles to old history if --only-missing key present: 2807 if onlyMissing and tempOld is not None and lastTime is not None: 2808 index = 0 # find start index in tempHistory data: 2809 2810 for i, item in tempHistory.iterrows(): 2811 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2812 2813 if curTime == lastTime: 2814 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2815 index = i 2816 printCount = index + 1 2817 break 2818 2819 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2820 2821 else: 2822 history = tempHistory # if no `--only-missing` key then load full data from server 2823 2824 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2825 2826 if history is not None and not history.empty: 2827 if show: 2828 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2829 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2830 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2831 )) 2832 2833 else: 2834 uLogger.warning("Received an empty candles history!") 2835 2836 if self.historyFile is not None: 2837 if history is not None and not history.empty: 2838 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2839 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2840 2841 else: 2842 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2843 2844 else: 2845 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2846 2847 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2849 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2850 """ 2851 Load candles history from csv-file and return Pandas DataFrame object. 2852 2853 See also: `History()` and `ShowHistoryChart()` methods. 2854 2855 :param filePath: path to csv-file to open. 2856 """ 2857 loadedHistory = None # init candles data object 2858 2859 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2860 2861 if os.path.exists(filePath): 2862 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2863 2864 tfStr = self.priceModel.FormattedDelta( 2865 self.priceModel.timeframe, 2866 "{days} days {hours}h {minutes}m {seconds}s", 2867 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2868 self.priceModel.timeframe, 2869 "{hours}h {minutes}m {seconds}s", 2870 ) 2871 2872 if loadedHistory is not None and not loadedHistory.empty: 2873 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2874 len(loadedHistory), 2875 tfStr, 2876 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2877 ) 2878 2879 else: 2880 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2881 2882 else: 2883 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2884 2885 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2887 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2888 """ 2889 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2890 2891 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2892 Default: `index.html` (both for interact and non-interact candlesticks chart). 2893 2894 See also: `History()` and `LoadHistory()` methods. 2895 2896 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2897 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2898 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2899 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2900 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2901 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2902 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2903 """ 2904 if isinstance(candles, str): 2905 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2906 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2907 2908 elif isinstance(candles, pd.DataFrame): 2909 self.priceModel.prices = candles # set candles chain from variable 2910 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2911 2912 if "datetime" not in candles.columns: 2913 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2914 2915 else: 2916 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2917 raise Exception("Incorrect value") 2918 2919 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2920 2921 if interact: 2922 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2923 2924 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2925 2926 else: 2927 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2928 2929 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2930 2931 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2933 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2934 """ 2935 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2936 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2937 2938 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2939 2940 :param operation: string "Buy" or "Sell". 2941 :param lots: volume, integer count of lots >= 1. 2942 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2943 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2944 :param expDate: string "Undefined" by default or local date in future, 2945 it is a string with format `%Y-%m-%d %H:%M:%S`. 2946 :return: JSON with response from broker server. 2947 """ 2948 if self.accountId is None or not self.accountId: 2949 uLogger.error("Variable `accountId` must be defined for using this method!") 2950 raise Exception("Account ID required") 2951 2952 if operation is None or not operation or operation not in ("Buy", "Sell"): 2953 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2954 raise Exception("Incorrect value") 2955 2956 if lots is None or lots < 1: 2957 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2958 lots = 1 2959 2960 if tp is None or tp < 0: 2961 tp = 0 2962 2963 if sl is None or sl < 0: 2964 sl = 0 2965 2966 if expDate is None or not expDate: 2967 expDate = "Undefined" 2968 2969 if not (self.ticker or self.figi): 2970 uLogger.error("Ticker or FIGI must be defined!") 2971 raise Exception("Ticker or FIGI required") 2972 2973 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2974 self.ticker = instrument["ticker"] 2975 self.figi = instrument["figi"] 2976 2977 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2978 2979 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2980 self.body = str({ 2981 "figi": self.figi, 2982 "quantity": str(lots), 2983 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2984 "accountId": str(self.accountId), 2985 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2986 }) 2987 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2988 2989 if "orderId" in response.keys(): 2990 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2991 operation, response["orderId"], 2992 self.ticker, self.figi, lots, 2993 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2994 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2995 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2996 )) 2997 2998 if tp > 0: 2999 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3000 3001 if sl > 0: 3002 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3003 3004 else: 3005 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 3006 3007 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3009 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3010 """ 3011 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3012 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3013 3014 See also: `Order()` and `Trade()` docstrings. 3015 3016 :param lots: volume, integer count of lots >= 1. 3017 :param tp: float > 0, take profit price of stop-order. 3018 :param sl: float > 0, stop loss price of stop-order. 3019 :param expDate: it's a local date in future. 3020 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3021 :return: JSON with response from broker server. 3022 """ 3023 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3025 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3026 """ 3027 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3028 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3029 3030 See also: `Order()` and `Trade()` docstrings. 3031 3032 :param lots: volume, integer count of lots >= 1. 3033 :param tp: float > 0, take profit price of stop-order. 3034 :param sl: float > 0, stop loss price of stop-order. 3035 :param expDate: it's a local date in the future. 3036 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3037 :return: JSON with response from broker server. 3038 """ 3039 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3041 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3042 """ 3043 Close position of given instruments. 3044 3045 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3046 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3047 This avoids unnecessary downloading data from the server. 3048 """ 3049 if instruments is None or not instruments: 3050 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3051 raise Exception("Ticker or FIGI required") 3052 3053 if isinstance(instruments, str): 3054 instruments = [instruments] 3055 3056 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3057 if uniqueInstruments: 3058 if portfolio is None or not portfolio: 3059 portfolio = self.Overview(show=False) 3060 3061 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3062 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3063 3064 for self.figi in uniqueInstruments: 3065 if self.figi not in allOpened: 3066 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3067 continue 3068 3069 # search open trade info about instrument by ticker: 3070 instrument = {} 3071 for iType in TKS_INSTRUMENTS: 3072 if instrument: 3073 break 3074 3075 for item in portfolio["stat"][iType]: 3076 if item["figi"] == self.figi: 3077 instrument = item 3078 break 3079 3080 if instrument: 3081 self.ticker = instrument["ticker"] 3082 self.figi = instrument["figi"] 3083 3084 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3085 self.ticker, 3086 self.figi, 3087 int(instrument["volume"]), 3088 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3089 )) 3090 3091 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3092 3093 if tradeLots > 0: 3094 if instrument["blocked"] > 0: 3095 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3096 instrument["blocked"], 3097 self.ticker, 3098 tradeLots, 3099 )) 3100 3101 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3102 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3103 3104 else: 3105 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3107 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3108 """ 3109 Close all positions of given instruments with defined type. 3110 3111 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3112 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3113 This avoids unnecessary downloading data from the server. 3114 """ 3115 if iType not in TKS_INSTRUMENTS: 3116 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3117 3118 else: 3119 if portfolio is None or not portfolio: 3120 portfolio = self.Overview(show=False) 3121 3122 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3123 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3124 3125 if tickers and portfolio: 3126 self.CloseTrades(tickers, portfolio) 3127 3128 else: 3129 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3131 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3132 """ 3133 Universal method to create market or limit orders with all available parameters for current `accountId`. 3134 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3135 3136 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3137 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3138 3139 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3140 then broker immediately open market order as you can do simple --buy or --sell operations! 3141 3142 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3143 When current price will go up or down to target price value then broker opens a limit order. 3144 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3145 3146 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3147 3148 :param operation: string "Buy" or "Sell". 3149 :param orderType: string "Limit" or "Stop". 3150 :param lots: volume, integer count of lots >= 1. 3151 :param targetPrice: target price > 0. This is open trade price for limit order. 3152 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3153 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3154 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3155 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3156 Stop loss order always executed by market price. 3157 :param expDate: string "Undefined" by default or local date in future. 3158 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3159 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3160 A limit order has no expiration date, it lasts until the end of the trading day. 3161 :return: JSON with response from broker server. 3162 """ 3163 if self.accountId is None or not self.accountId: 3164 uLogger.error("Variable `accountId` must be defined for using this method!") 3165 raise Exception("Account ID required") 3166 3167 if operation is None or not operation or operation not in ("Buy", "Sell"): 3168 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3169 raise Exception("Incorrect value") 3170 3171 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3172 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3173 raise Exception("Incorrect value") 3174 3175 if lots is None or lots < 1: 3176 uLogger.error("You must define trade volume > 0: integer count of lots!") 3177 raise Exception("Incorrect value") 3178 3179 if targetPrice is None or targetPrice <= 0: 3180 uLogger.error("Target price for limit-order must be greater than 0!") 3181 raise Exception("Incorrect value") 3182 3183 if limitPrice is None or limitPrice <= 0: 3184 limitPrice = targetPrice 3185 3186 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3187 stopType = "Limit" 3188 3189 if expDate is None or not expDate: 3190 expDate = "Undefined" 3191 3192 if not (self.ticker or self.figi): 3193 uLogger.error("Tocker or FIGI must be defined!") 3194 raise Exception("Ticker or FIGI required") 3195 3196 response = {} 3197 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3198 self.ticker = instrument["ticker"] 3199 self.figi = instrument["figi"] 3200 3201 if orderType == "Limit": 3202 uLogger.debug( 3203 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3204 self.ticker, self.figi, 3205 operation, lots, targetPrice, instrument["currency"], 3206 )) 3207 3208 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3209 self.body = str({ 3210 "figi": self.figi, 3211 "quantity": str(lots), 3212 "price": FloatToNano(targetPrice), 3213 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3214 "accountId": str(self.accountId), 3215 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3216 }) 3217 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3218 3219 if "orderId" in response.keys(): 3220 uLogger.info( 3221 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3222 response["orderId"], 3223 self.ticker, self.figi, 3224 operation, lots, targetPrice, instrument["currency"], 3225 )) 3226 3227 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3228 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3229 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3230 targetPrice, instrument["currency"], 3231 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3232 )) 3233 3234 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3235 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3236 targetPrice, instrument["currency"], 3237 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3238 )) 3239 3240 else: 3241 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3242 3243 if orderType == "Stop": 3244 uLogger.debug( 3245 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3246 self.ticker, self.figi, 3247 operation, lots, 3248 targetPrice, instrument["currency"], 3249 limitPrice, instrument["currency"], 3250 stopType, expDate, 3251 )) 3252 3253 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3254 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3255 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3256 3257 body = { 3258 "figi": self.figi, 3259 "quantity": str(lots), 3260 "price": FloatToNano(limitPrice), 3261 "stopPrice": FloatToNano(targetPrice), 3262 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3263 "accountId": str(self.accountId), 3264 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3265 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3266 } 3267 3268 if expDateUTC: 3269 body["expireDate"] = expDateUTC 3270 3271 self.body = str(body) 3272 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3273 3274 if "stopOrderId" in response.keys(): 3275 uLogger.info( 3276 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3277 response["stopOrderId"], 3278 self.ticker, self.figi, 3279 operation, lots, 3280 targetPrice, instrument["currency"], 3281 limitPrice, instrument["currency"], 3282 TKS_STOP_ORDER_TYPES[stopOrderType], 3283 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3284 )) 3285 3286 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3287 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3288 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3289 targetPrice, instrument["currency"], 3290 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3291 )) 3292 3293 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3294 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3295 targetPrice, instrument["currency"], 3296 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3297 )) 3298 3299 else: 3300 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3301 3302 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3304 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3305 """ 3306 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3307 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3308 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3309 See also: `Order()` docstring. 3310 3311 :param lots: volume, integer count of lots >= 1. 3312 :param targetPrice: target price > 0. This is open trade price for limit order. 3313 :return: JSON with response from broker server. 3314 """ 3315 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3317 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3318 """ 3319 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3320 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3321 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3322 target price value then broker opens a limit order. See also: `Order()` docstring. 3323 3324 :param lots: volume, integer count of lots >= 1. 3325 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3326 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3327 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3328 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3329 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3330 :param expDate: string "Undefined" by default or local date in future. 3331 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3332 This date is converting to UTC format for server. 3333 :return: JSON with response from broker server. 3334 """ 3335 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3337 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3338 """ 3339 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3340 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3341 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3342 See also: `Order()` docstring. 3343 3344 :param lots: volume, integer count of lots >= 1. 3345 :param targetPrice: target price > 0. This is open trade price for limit order. 3346 :return: JSON with response from broker server. 3347 """ 3348 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3350 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3351 """ 3352 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3353 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3354 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3355 target price value then broker opens a limit order. See also: `Order()` docstring. 3356 3357 :param lots: volume, integer count of lots >= 1. 3358 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3359 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3360 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3361 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3362 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3363 :param expDate: string "Undefined" by default or local date in future. 3364 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3365 This date is converting to UTC format for server. 3366 :return: JSON with response from broker server. 3367 """ 3368 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3370 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3371 """ 3372 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3373 3374 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3375 :param allOrdersIDs: pre-received lists of all active pending orders. 3376 This avoids unnecessary downloading data from the server. 3377 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3378 """ 3379 if self.accountId is None or not self.accountId: 3380 uLogger.error("Variable `accountId` must be defined for using this method!") 3381 raise Exception("Account ID required") 3382 3383 if orderIDs: 3384 if allOrdersIDs is None or not allOrdersIDs: 3385 rawOrders = self.RequestPendingOrders() 3386 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3387 3388 if allStopOrdersIDs is None or not allStopOrdersIDs: 3389 rawStopOrders = self.RequestStopOrders() 3390 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3391 3392 for orderID in orderIDs: 3393 idInPendingOrders = orderID in allOrdersIDs 3394 idInStopOrders = orderID in allStopOrdersIDs 3395 3396 if not (idInPendingOrders or idInStopOrders): 3397 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3398 continue 3399 3400 else: 3401 if idInPendingOrders: 3402 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3403 3404 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3405 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3406 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3407 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3408 3409 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3410 if self.moreDebug: 3411 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3412 3413 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3414 3415 else: 3416 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3417 3418 elif idInStopOrders: 3419 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3420 3421 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3422 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3423 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3424 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3425 3426 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3427 if self.moreDebug: 3428 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3429 3430 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3431 3432 else: 3433 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3434 3435 else: 3436 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3438 def CloseAllOrders(self) -> None: 3439 """ 3440 Gets a list of open pending and stop orders and cancel it all. 3441 """ 3442 rawOrders = self.RequestPendingOrders() 3443 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3444 lenOrders = len(allOrdersIDs) 3445 3446 rawStopOrders = self.RequestStopOrders() 3447 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3448 lenSOrders = len(allStopOrdersIDs) 3449 3450 if lenOrders > 0 or lenSOrders > 0: 3451 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3452 3453 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3454 3455 else: 3456 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3458 def CloseAll(self, *args) -> None: 3459 """ 3460 Close all available (not blocked) opened trades and orders. 3461 3462 Also, you can select one or more keywords case-insensitive: 3463 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3464 3465 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3466 """ 3467 overview = self.Overview(show=False) # get all open trades info 3468 3469 if len(args) == 0: 3470 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3471 self.CloseAllOrders() # close all pending and stop orders 3472 3473 for iType in TKS_INSTRUMENTS: 3474 if iType != "Currencies": 3475 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3476 3477 else: 3478 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3479 lowerArgs = [x.lower() for x in args] 3480 3481 if "orders" in lowerArgs: 3482 self.CloseAllOrders() # close all pending and stop orders 3483 3484 for iType in TKS_INSTRUMENTS: 3485 if iType.lower() in lowerArgs and iType != "Currencies": 3486 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3488 @staticmethod 3489 def ParseOrderParameters(operation, **inputParameters): 3490 """ 3491 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3492 3493 :param operation: string "Buy" or "Sell". 3494 :param inputParameters: this is dict of strings that looks like this 3495 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3496 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3497 "prices" key: one or more prices to open limit-orders 3498 Counts of values in lots and prices lists must be equals! 3499 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3500 """ 3501 # TODO: update order grid work with api v2 3502 pass 3503 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3504 # 3505 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3506 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3507 # raise Exception("Incorrect value") 3508 # 3509 # if "l" in inputParameters.keys(): 3510 # inputParameters["lots"] = inputParameters.pop("l") 3511 # 3512 # if "p" in inputParameters.keys(): 3513 # inputParameters["prices"] = inputParameters.pop("p") 3514 # 3515 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3516 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3517 # raise Exception("Incorrect value") 3518 # 3519 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3520 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3521 # 3522 # if len(lots) != len(prices): 3523 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3524 # raise Exception("Incorrect value") 3525 # 3526 # uLogger.debug("Extracted parameters for orders:") 3527 # uLogger.debug("lots = {}".format(lots)) 3528 # uLogger.debug("prices = {}".format(prices)) 3529 # 3530 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3531 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3532 # uLogger.debug("Order parameters: {}".format(result)) 3533 # 3534 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3536 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3537 """ 3538 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3539 3540 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3541 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3542 """ 3543 result = False 3544 msg = "Instrument not defined!" 3545 3546 if portfolio is None or not portfolio: 3547 portfolio = self.Overview(show=False) 3548 3549 if self.ticker: 3550 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3551 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3552 3553 for iType in TKS_INSTRUMENTS: 3554 for instrument in portfolio["stat"][iType]: 3555 if instrument["ticker"] == self.ticker: 3556 result = True 3557 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3558 break 3559 3560 elif self.figi: 3561 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3562 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3563 3564 for iType in TKS_INSTRUMENTS: 3565 for instrument in portfolio["stat"][iType]: 3566 if instrument["figi"] == self.figi: 3567 result = True 3568 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3569 break 3570 3571 else: 3572 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3573 3574 uLogger.debug(msg) 3575 3576 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3578 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3579 """ 3580 Returns instrument from the user's portfolio if it presents there. 3581 Instrument must be defined by `ticker` (highly priority) or `figi`. 3582 3583 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3584 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3585 """ 3586 result = None 3587 msg = "Instrument not defined!" 3588 3589 if portfolio is None or not portfolio: 3590 portfolio = self.Overview(show=False) 3591 3592 if self.ticker: 3593 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3594 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3595 3596 for iType in TKS_INSTRUMENTS: 3597 for instrument in portfolio["stat"][iType]: 3598 if instrument["ticker"] == self.ticker: 3599 result = instrument 3600 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3601 break 3602 3603 elif self.figi: 3604 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3605 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3606 3607 for iType in TKS_INSTRUMENTS: 3608 for instrument in portfolio["stat"][iType]: 3609 if instrument["figi"] == self.figi: 3610 result = instrument 3611 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3612 break 3613 3614 else: 3615 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3616 3617 uLogger.debug(msg) 3618 3619 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3621 def RequestLimits(self) -> dict: 3622 """ 3623 Method for obtaining the available funds for withdrawal for current `accountId`. 3624 3625 See also: 3626 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3627 - `OverviewLimits()` method 3628 3629 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3630 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3631 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3632 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3633 """ 3634 if self.accountId is None or not self.accountId: 3635 uLogger.error("Variable `accountId` must be defined for using this method!") 3636 raise Exception("Account ID required") 3637 3638 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3639 3640 self.body = str({"accountId": self.accountId}) 3641 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3642 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3643 3644 if self.moreDebug: 3645 uLogger.debug("Records about available funds for withdrawal successfully received") 3646 3647 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3649 def OverviewLimits(self, show: bool = False) -> dict: 3650 """ 3651 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3652 3653 See also: `RequestLimits()`. 3654 3655 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3656 :return: dict with raw parsed data from server and some calculated statistics about it. 3657 """ 3658 if self.accountId is None or not self.accountId: 3659 uLogger.error("Variable `accountId` must be defined for using this method!") 3660 raise Exception("Account ID required") 3661 3662 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3663 3664 view = { 3665 "rawLimits": rawLimits, 3666 "limits": { # parsed data for every currency: 3667 "money": { # this is an array of portfolio currency positions 3668 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3669 }, 3670 "blocked": { # this is an array of blocked currency 3671 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3672 }, 3673 "blockedGuarantee": { # this is locked money under collateral for futures 3674 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3675 }, 3676 }, 3677 } 3678 3679 # --- Prepare text table with limits in human-readable format: 3680 if show: 3681 info = [ 3682 "# Withdrawal limits\n\n", 3683 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3684 "* **Account ID:** [{}]\n".format(self.accountId), 3685 ] 3686 3687 if view["limits"]["money"]: 3688 info.extend([ 3689 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3690 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3691 ]) 3692 3693 else: 3694 info.append("\nNo withdrawal limits\n") 3695 3696 for curr in view["limits"]["money"].keys(): 3697 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3698 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3699 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3700 3701 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3702 "[{}]".format(curr), 3703 "{:.2f}".format(view["limits"]["money"][curr]), 3704 "{:.2f}".format(availableMoney), 3705 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3706 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3707 ) 3708 3709 if curr == "rub": 3710 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3711 3712 else: 3713 info.append(infoStr) 3714 3715 infoText = "".join(info) 3716 3717 uLogger.info(infoText) 3718 3719 if self.withdrawalLimitsFile: 3720 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3721 fH.write(infoText) 3722 3723 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3724 3725 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3727 def RequestAccounts(self) -> dict: 3728 """ 3729 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3730 3731 See also: 3732 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3733 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3734 - `OverviewUserInfo()` method 3735 3736 :return: dict with raw data from server that contains accounts info. Example of dict: 3737 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3738 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3739 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3740 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3741 """ 3742 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3743 3744 self.body = str({}) 3745 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3746 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3747 3748 if self.moreDebug: 3749 uLogger.debug("Records about available accounts successfully received") 3750 3751 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3753 def RequestUserInfo(self) -> dict: 3754 """ 3755 Method for requesting common user's information. 3756 3757 See also: 3758 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3759 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3760 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3761 - `OverviewUserInfo()` method 3762 3763 :return: dict with raw data from server that contains user's information. Example of dict: 3764 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3765 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3766 """ 3767 uLogger.debug("Requesting common user's information. Wait, please...") 3768 3769 self.body = str({}) 3770 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3771 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3772 3773 if self.moreDebug: 3774 uLogger.debug("Records about current user successfully received") 3775 3776 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3778 def RequestMarginStatus(self, accountId: str = None) -> dict: 3779 """ 3780 Method for requesting margin calculation for defined account ID. 3781 3782 See also: 3783 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3784 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3785 - `OverviewUserInfo()` method 3786 3787 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3788 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3789 Example of responses: 3790 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3791 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3792 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3793 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3794 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3795 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3796 """ 3797 if accountId is None or not accountId: 3798 if self.accountId is None or not self.accountId: 3799 uLogger.error("Variable `accountId` must be defined for using this method!") 3800 raise Exception("Account ID required") 3801 3802 else: 3803 accountId = self.accountId # use `self.accountId` (main ID) by default 3804 3805 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3806 3807 self.body = str({"accountId": accountId}) 3808 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3809 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3810 3811 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3812 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3813 rawMargin = {} 3814 3815 else: 3816 if self.moreDebug: 3817 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3818 3819 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3821 def RequestTariffLimits(self) -> dict: 3822 """ 3823 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3824 3825 See also: 3826 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3827 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3828 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3829 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3830 - `OverviewUserInfo()` method 3831 3832 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3833 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3834 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3835 """ 3836 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3837 3838 self.body = str({}) 3839 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3840 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3841 3842 if self.moreDebug: 3843 uLogger.debug("Records with limits of current tariff successfully received") 3844 3845 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3847 def RequestBondCoupons(self, iJSON: dict) -> dict: 3848 """ 3849 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3850 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3851 All dates are in UTC timezone. 3852 3853 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3854 Documentation: 3855 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3856 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3857 3858 See also: `ExtendBondsData()`. 3859 3860 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3861 If raw iJSON is not data of bond then server returns an error [400] with message: 3862 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3863 :return: dictionary with bond payment calendar. Response example 3864 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3865 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3866 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3867 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3868 """ 3869 if iJSON["figi"] is None or not iJSON["figi"]: 3870 uLogger.error("FIGI must be defined for using this method!") 3871 raise Exception("FIGI required") 3872 3873 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3874 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3875 3876 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3877 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3878 self.figi, 3879 startDate, 3880 endDate, 3881 )) 3882 3883 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3884 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3885 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3886 3887 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3888 uLogger.warning("Instrument type is not bond!") 3889 3890 else: 3891 if self.moreDebug: 3892 uLogger.debug("Records about bond payment calendar successfully received") 3893 3894 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3896 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3897 """ 3898 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3899 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3900 coupon yields, current yields and some statistics etc. 3901 3902 WARNING! This is too long operation if a lot of bonds requested from broker server. 3903 3904 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3905 3906 :param instruments: list of strings with tickers or FIGIs. 3907 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3908 for further used by data scientists or stock analytics. 3909 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3910 In XLSX-file and Pandas DataFrame fields mean: 3911 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3912 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3913 """ 3914 if instruments is None or not instruments: 3915 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3916 raise Exception("Ticker or FIGI required") 3917 3918 if isinstance(instruments, str): 3919 instruments = [instruments] 3920 3921 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3922 3923 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3924 3925 iCount = len(uniqueInstruments) 3926 tooLong = iCount >= 20 3927 if tooLong: 3928 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3929 3930 bonds = None 3931 for i, self.figi in enumerate(uniqueInstruments): 3932 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3933 3934 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3935 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3936 rawBond = self.SearchByFIGI(requestPrice=True) 3937 3938 # Widen raw data with UTC current time (iData["actualDateTime"]): 3939 actualDate = datetime.now(tzutc()) 3940 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3941 3942 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3943 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3944 3945 # Replace some values with human-readable: 3946 iData["nominalCurrency"] = iData["nominal"]["currency"] 3947 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3948 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3949 iData["aciCurrency"] = iData["aciValue"]["currency"] 3950 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3951 iData["issueSize"] = int(iData["issueSize"]) 3952 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3953 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3954 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3955 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3956 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3957 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3958 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3959 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3960 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3961 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3962 3963 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3964 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3965 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3966 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3967 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3968 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3969 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3970 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3971 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3972 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3973 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3974 3975 # Widen raw data with calendar data from `rawCalendar` values: 3976 calendarData = [] 3977 if "events" in iData["rawCalendar"].keys(): 3978 for item in iData["rawCalendar"]["events"]: 3979 calendarData.append({ 3980 "couponDate": item["couponDate"], 3981 "couponNumber": int(item["couponNumber"]), 3982 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3983 "payCurrency": item["payOneBond"]["currency"], 3984 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3985 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3986 "couponStartDate": item["couponStartDate"], 3987 "couponEndDate": item["couponEndDate"], 3988 "couponPeriod": item["couponPeriod"], 3989 }) 3990 3991 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3992 if "maturityDate" not in iData.keys(): 3993 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3994 3995 # Widen raw data with Coupon Rate. 3996 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3997 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3998 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3999 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4000 4001 # Widen raw data with Yield to Maturity (YTM) on current date. 4002 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4003 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4004 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4005 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4006 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4007 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4008 4009 iData["calendar"] = calendarData # adds calendar at the end 4010 4011 # Remove not used data: 4012 iData.pop("uid") 4013 iData.pop("positionUid") 4014 iData.pop("currentPrice") 4015 iData.pop("rawCalendar") 4016 4017 colNames = list(iData.keys()) 4018 if bonds is None: 4019 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4020 4021 else: 4022 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4023 4024 else: 4025 uLogger.warning("Instrument is not a bond!") 4026 4027 processed = round(100 * (i + 1) / iCount, 1) 4028 if tooLong and processed % 5 == 0: 4029 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4030 4031 else: 4032 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4033 4034 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4035 4036 # Saving bonds from Pandas DataFrame to XLSX sheet: 4037 if xlsx and self.bondsXLSXFile: 4038 with pd.ExcelWriter( 4039 path=self.bondsXLSXFile, 4040 date_format=TKS_DATE_FORMAT, 4041 datetime_format=TKS_DATE_TIME_FORMAT, 4042 mode="w", 4043 ) as writer: 4044 bonds.to_excel( 4045 writer, 4046 sheet_name="Extended bonds data", 4047 index=True, 4048 encoding="UTF-8", 4049 freeze_panes=(1, 1), 4050 ) # saving as XLSX-file with freeze first row and column as headers 4051 4052 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4053 4054 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4056 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4057 """ 4058 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4059 4060 WARNING! This is too long operation if a lot of bonds requested from broker server. 4061 4062 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4063 4064 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4065 extended information about bonds: main info, current prices, bond payment calendar, 4066 coupon yields, current yields and some statistics etc. 4067 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4068 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4069 for further used by data scientists or stock analytics. 4070 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4071 """ 4072 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4073 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4074 4075 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4076 4077 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4078 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4079 calendar = None 4080 for bond in extBonds.iterrows(): 4081 for item in bond[1]["calendar"]: 4082 cData = { 4083 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4084 "couponDate": item["couponDate"], 4085 "figi": bond[1]["figi"], 4086 "ticker": bond[1]["ticker"], 4087 "name": bond[1]["name"], 4088 "couponNumber": item["couponNumber"], 4089 "payOneBond": item["payOneBond"], 4090 "payCurrency": item["payCurrency"], 4091 "couponType": item["couponType"], 4092 "couponPeriod": item["couponPeriod"], 4093 "fixDate": item["fixDate"], 4094 "couponStartDate": item["couponStartDate"], 4095 "couponEndDate": item["couponEndDate"], 4096 } 4097 4098 if calendar is None: 4099 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4100 4101 else: 4102 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4103 4104 if calendar is not None: 4105 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4106 4107 # Saving calendar from Pandas DataFrame to XLSX sheet: 4108 if xlsx: 4109 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4110 4111 with pd.ExcelWriter( 4112 path=xlsxCalendarFile, 4113 date_format=TKS_DATE_FORMAT, 4114 datetime_format=TKS_DATE_TIME_FORMAT, 4115 mode="w", 4116 ) as writer: 4117 humanReadable = calendar.copy(deep=True) 4118 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4119 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4120 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4121 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4122 humanReadable.columns = colNames # human-readable column names 4123 4124 humanReadable.to_excel( 4125 writer, 4126 sheet_name="Bond payments calendar", 4127 index=False, 4128 encoding="UTF-8", 4129 freeze_panes=(1, 2), 4130 ) # saving as XLSX-file with freeze first row and column as headers 4131 4132 del humanReadable # release df in memory 4133 4134 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4135 4136 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4138 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4139 """ 4140 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4141 Also, creates Markdown file with calendar data, `calendar.md` by default. 4142 4143 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4144 4145 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4146 extended information about bonds: main info, current prices, bond payment calendar, 4147 coupon yields, current yields and some statistics etc. 4148 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4149 :param show: if `True` then also printing bonds payment calendar to the console, 4150 otherwise save to file `calendarFile` only. `False` by default. 4151 :return: multilines text in Markdown format with bonds payment calendar as a table. 4152 """ 4153 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4154 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4155 4156 infoText = "# Bond payments calendar\n\n" 4157 4158 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4159 4160 if not (calendar is None or calendar.empty): 4161 splitLine = "| | | | | | | | | |\n" 4162 4163 info = [ 4164 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4165 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4166 ] 4167 4168 newMonth = False 4169 notOneBond = calendar["figi"].nunique() > 1 4170 for i, bond in enumerate(calendar.iterrows()): 4171 if newMonth and notOneBond: 4172 info.append(splitLine) 4173 4174 info.append( 4175 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4176 " √" if bond[1]["paid"] else " —", 4177 bond[1]["couponDate"].split("T")[0], 4178 bond[1]["figi"], 4179 bond[1]["ticker"], 4180 bond[1]["couponNumber"], 4181 "{} {}".format( 4182 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4183 bond[1]["payCurrency"], 4184 ), 4185 bond[1]["couponType"], 4186 bond[1]["couponPeriod"], 4187 bond[1]["fixDate"].split("T")[0], 4188 ) 4189 ) 4190 4191 if i < len(calendar.values) - 1: 4192 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4193 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4194 newMonth = False if curDate.month == nextDate.month else True 4195 4196 else: 4197 newMonth = False 4198 4199 infoText += "".join(info) 4200 4201 if show: 4202 uLogger.info("{}".format(infoText)) 4203 4204 if self.calendarFile is not None: 4205 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4206 fH.write(infoText) 4207 4208 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4209 4210 else: 4211 infoText += "No data\n" 4212 4213 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4215 def OverviewAccounts(self, show: bool = False) -> dict: 4216 """ 4217 Method for parsing and show simple table with all available user accounts. 4218 4219 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4220 4221 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4222 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4223 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4224 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4225 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4226 "closed": "—", "access": "Full access" }, ...}}` 4227 """ 4228 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4229 4230 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4231 accounts = { 4232 item["id"]: { 4233 "type": TKS_ACCOUNT_TYPES[item["type"]], 4234 "name": item["name"], 4235 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4236 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4237 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4238 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4239 } for item in rawAccounts["accounts"] 4240 } 4241 4242 # Raw and parsed data with some fields replaced in "stat" section: 4243 view = { 4244 "rawAccounts": rawAccounts, 4245 "stat": accounts, 4246 } 4247 4248 # --- Prepare simple text table with only accounts data in human-readable format: 4249 if show: 4250 info = [ 4251 "# User accounts\n\n", 4252 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4253 "| Account ID | Type | Status | Name |\n", 4254 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4255 ] 4256 4257 for account in view["stat"].keys(): 4258 info.extend([ 4259 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4260 account, 4261 view["stat"][account]["type"], 4262 view["stat"][account]["status"], 4263 view["stat"][account]["name"], 4264 ) 4265 ]) 4266 4267 infoText = "".join(info) 4268 4269 uLogger.info(infoText) 4270 4271 if self.userAccountsFile: 4272 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4273 fH.write(infoText) 4274 4275 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4276 4277 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4279 def OverviewUserInfo(self, show: bool = False) -> dict: 4280 """ 4281 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4282 4283 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4284 4285 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4286 :return: dict with raw parsed data from server and some calculated statistics about it. 4287 """ 4288 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4289 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4290 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4291 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4292 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4293 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4294 4295 # This is dict with parsed common user data: 4296 userInfo = { 4297 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4298 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4299 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4300 "tariff": rawUserInfo["tariff"], 4301 } 4302 4303 # This is an array of dict with parsed margin statuses for every account IDs: 4304 margins = {} 4305 for accountId in accounts.keys(): 4306 if rawMargins[accountId]: 4307 margins[accountId] = { 4308 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4309 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4310 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4311 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4312 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4313 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4314 } 4315 4316 else: 4317 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4318 4319 unary = {} # unary-connection limits 4320 for item in rawTariffLimits["unaryLimits"]: 4321 if item["limitPerMinute"] in unary.keys(): 4322 unary[item["limitPerMinute"]].extend(item["methods"]) 4323 4324 else: 4325 unary[item["limitPerMinute"]] = item["methods"] 4326 4327 stream = {} # stream-connection limits 4328 for item in rawTariffLimits["streamLimits"]: 4329 if item["limit"] in stream.keys(): 4330 stream[item["limit"]].extend(item["streams"]) 4331 4332 else: 4333 stream[item["limit"]] = item["streams"] 4334 4335 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4336 limits = { 4337 "unary": unary, 4338 "stream": stream, 4339 } 4340 4341 # Raw and parsed data as an output result: 4342 view = { 4343 "rawUserInfo": rawUserInfo, 4344 "rawAccounts": rawAccounts, 4345 "rawMargins": rawMargins, 4346 "rawTariffLimits": rawTariffLimits, 4347 "stat": { 4348 "userInfo": userInfo, 4349 "accounts": accounts, 4350 "margins": margins, 4351 "limits": limits, 4352 }, 4353 } 4354 4355 # --- Prepare text table with user information in human-readable format: 4356 if show: 4357 info = [ 4358 "# Full user information\n\n", 4359 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4360 "## Common information\n\n", 4361 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4362 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4363 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4364 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4365 "\n## User accounts\n\n", 4366 ] 4367 4368 for account in view["stat"]["accounts"].keys(): 4369 info.extend([ 4370 "### ID: [{}]\n\n".format(account), 4371 "| Parameters | Values |\n", 4372 "|----------------------|--------------------------------------------------------------|\n", 4373 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4374 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4375 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4376 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4377 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4378 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4379 ]) 4380 4381 if margins[account]: 4382 info.extend([ 4383 "| Margin status: | Enabled |\n", 4384 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4385 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4386 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4387 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4388 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4389 ]) 4390 4391 else: 4392 info.append("| Margin status: | Disabled |\n\n") 4393 4394 info.extend([ 4395 "\n## Current user tariff limits\n", 4396 "\nSee also:\n", 4397 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4398 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4399 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4400 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4401 "\n### Unary limits\n", 4402 ]) 4403 4404 if unary: 4405 for key, values in sorted(unary.items()): 4406 info.append("\n* Max requests per minute: {}\n".format(key)) 4407 4408 for value in values: 4409 info.append(" - {}\n".format(value)) 4410 4411 else: 4412 info.append("\nNot available\n") 4413 4414 info.append("\n### Stream limits\n") 4415 4416 if stream: 4417 for key, values in sorted(stream.items()): 4418 info.append("\n* Max stream connections: {}\n".format(key)) 4419 4420 for value in values: 4421 info.append(" - {}\n".format(value)) 4422 4423 else: 4424 info.append("\nNot available\n") 4425 4426 infoText = "".join(info) 4427 4428 uLogger.info(infoText) 4429 4430 if self.userInfoFile: 4431 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4432 fH.write(infoText) 4433 4434 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4435 4436 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4439class Args: 4440 """ 4441 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4442 """ 4443 def __init__(self, **kwargs): 4444 self.__dict__.update(kwargs) 4445 4446 def __getattr__(self, item): 4447 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4450def ParseArgs(): 4451 """This function get and parse command line keys.""" 4452 parser = ArgumentParser() # command-line string parser 4453 4454 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4455 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4456 4457 # --- options: 4458 4459 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4460 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4461 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4462 4463 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4464 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4465 4466 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4467 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4468 4469 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4470 4471 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4472 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4473 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4474 4475 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4476 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4477 4478 # --- commands: 4479 4480 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4481 4482 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4483 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4484 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4485 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4486 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4487 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4488 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4489 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4490 4491 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4492 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4493 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4494 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4495 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4496 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4497 4498 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4499 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4500 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4501 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4502 4503 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4504 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4505 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4506 4507 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4508 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4509 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4510 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4511 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4512 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4513 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4514 4515 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4516 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4517 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4518 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4519 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4520 4521 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4522 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4523 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4524 4525 cmdArgs = parser.parse_args() 4526 return cmdArgs
This function get and parse command line keys.
4529def Main(**kwargs): 4530 """ 4531 Main function for work with TKSBrokerAPI in the console. 4532 4533 See examples: 4534 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4535 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4536 """ 4537 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4538 4539 if args.debug_level: 4540 uLogger.level = 10 # always debug level by default 4541 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4542 4543 exitCode = 0 4544 start = datetime.now(tzutc()) 4545 uLogger.debug("=-" * 50) 4546 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4547 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4548 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4549 )) 4550 4551 # trying to calculate full current version: 4552 buildVersion = __version__ 4553 try: 4554 v = version("tksbrokerapi") 4555 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4556 4557 except Exception: 4558 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4559 4560 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4561 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4562 4563 try: 4564 if args.version: 4565 print("TKSBrokerAPI {}".format(buildVersion)) 4566 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4567 4568 else: 4569 # Init class for trading with Tinkoff Broker: 4570 trader = TinkoffBrokerServer( 4571 token=args.token, 4572 accountId=args.account_id, 4573 useCache=not args.no_cache, 4574 ) 4575 4576 # --- set some options: 4577 4578 if args.more: 4579 trader.moreDebug = True 4580 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4581 4582 if args.ticker: 4583 ticker = args.ticker.upper() # Tickers may be upper case only 4584 4585 if ticker in trader.aliasesKeys: 4586 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4587 4588 else: 4589 trader.ticker = ticker 4590 4591 if args.figi: 4592 trader.figi = args.figi.upper() # FIGIs may be upper case only 4593 4594 if args.depth is not None: 4595 trader.depth = args.depth 4596 4597 # --- do one command: 4598 4599 if args.list: 4600 if args.output is not None: 4601 trader.instrumentsFile = args.output 4602 4603 trader.ShowInstrumentsInfo(show=True) 4604 4605 elif args.list_xlsx: 4606 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4607 4608 elif args.bonds_xlsx is not None: 4609 if args.output is not None: 4610 trader.bondsXLSXFile = args.output 4611 4612 if len(args.bonds_xlsx) == 0: 4613 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4614 4615 else: 4616 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4617 4618 elif args.search: 4619 if args.output is not None: 4620 trader.searchResultsFile = args.output 4621 4622 trader.SearchInstruments(pattern=args.search[0], show=True) 4623 4624 elif args.info: 4625 if not (args.ticker or args.figi): 4626 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4627 raise Exception("Ticker or FIGI required") 4628 4629 if args.output is not None: 4630 trader.infoFile = args.output 4631 4632 if args.ticker: 4633 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4634 4635 else: 4636 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4637 4638 elif args.calendar is not None: 4639 if args.output is not None: 4640 trader.calendarFile = args.output 4641 4642 if len(args.calendar) == 0: 4643 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4644 4645 else: 4646 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4647 4648 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4649 4650 elif args.price: 4651 if not (args.ticker or args.figi): 4652 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4653 raise Exception("Ticker or FIGI required") 4654 4655 trader.GetCurrentPrices(show=True) 4656 4657 elif args.prices is not None: 4658 if args.output is not None: 4659 trader.pricesFile = args.output 4660 4661 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4662 4663 elif args.overview: 4664 if args.output is not None: 4665 trader.overviewFile = args.output 4666 4667 trader.Overview(show=True, details="full") 4668 4669 elif args.overview_digest: 4670 if args.output is not None: 4671 trader.overviewDigestFile = args.output 4672 4673 trader.Overview(show=True, details="digest") 4674 4675 elif args.overview_positions: 4676 if args.output is not None: 4677 trader.overviewPositionsFile = args.output 4678 4679 trader.Overview(show=True, details="positions") 4680 4681 elif args.overview_orders: 4682 if args.output is not None: 4683 trader.overviewOrdersFile = args.output 4684 4685 trader.Overview(show=True, details="orders") 4686 4687 elif args.overview_analytics: 4688 if args.output is not None: 4689 trader.overviewAnalyticsFile = args.output 4690 4691 trader.Overview(show=True, details="analytics") 4692 4693 elif args.overview_calendar: 4694 if args.output is not None: 4695 trader.overviewAnalyticsFile = args.output 4696 4697 trader.Overview(show=True, details="calendar") 4698 4699 elif args.deals is not None: 4700 if args.output is not None: 4701 trader.reportFile = args.output 4702 4703 if 0 <= len(args.deals) < 3: 4704 trader.Deals( 4705 start=args.deals[0] if len(args.deals) >= 1 else None, 4706 end=args.deals[1] if len(args.deals) == 2 else None, 4707 show=True, # Always show deals report in console 4708 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4709 ) 4710 4711 else: 4712 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4713 raise Exception("Incorrect value") 4714 4715 elif args.history is not None: 4716 if args.output is not None: 4717 trader.historyFile = args.output 4718 4719 if 0 <= len(args.history) < 3: 4720 dataReceived = trader.History( 4721 start=args.history[0] if len(args.history) >= 1 else None, 4722 end=args.history[1] if len(args.history) == 2 else None, 4723 interval="hour" if args.interval is None or not args.interval else args.interval, 4724 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4725 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4726 show=True, # shows all downloaded candles in console 4727 ) 4728 4729 if args.render_chart is not None and dataReceived is not None: 4730 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4731 4732 trader.ShowHistoryChart( 4733 candles=dataReceived, 4734 interact=iChart, 4735 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4736 ) 4737 4738 else: 4739 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4740 raise Exception("Incorrect value") 4741 4742 elif args.load_history is not None: 4743 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4744 4745 if args.render_chart is not None and histData is not None: 4746 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4747 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4748 4749 trader.ShowHistoryChart( 4750 candles=histData, 4751 interact=iChart, 4752 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4753 ) 4754 4755 elif args.trade is not None: 4756 if 1 <= len(args.trade) <= 5: 4757 trader.Trade( 4758 operation=args.trade[0], 4759 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4760 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4761 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4762 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4763 ) 4764 4765 else: 4766 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4767 4768 elif args.buy is not None: 4769 if 0 <= len(args.buy) <= 4: 4770 trader.Buy( 4771 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4772 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4773 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4774 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4775 ) 4776 4777 else: 4778 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4779 4780 elif args.sell is not None: 4781 if 0 <= len(args.sell) <= 4: 4782 trader.Sell( 4783 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4784 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4785 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4786 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4787 ) 4788 4789 else: 4790 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4791 4792 elif args.order: 4793 if 4 <= len(args.order) <= 7: 4794 trader.Order( 4795 operation=args.order[0], 4796 orderType=args.order[1], 4797 lots=int(args.order[2]), 4798 targetPrice=float(args.order[3]), 4799 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4800 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4801 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4802 ) 4803 4804 else: 4805 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4806 4807 elif args.buy_limit: 4808 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4809 4810 elif args.sell_limit: 4811 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4812 4813 elif args.buy_stop: 4814 if 2 <= len(args.buy_stop) <= 7: 4815 trader.BuyStop( 4816 lots=int(args.buy_stop[0]), 4817 targetPrice=float(args.buy_stop[1]), 4818 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4819 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4820 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4821 ) 4822 4823 else: 4824 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4825 4826 elif args.sell_stop: 4827 if 2 <= len(args.sell_stop) <= 7: 4828 trader.SellStop( 4829 lots=int(args.sell_stop[0]), 4830 targetPrice=float(args.sell_stop[1]), 4831 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4832 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4833 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4834 ) 4835 4836 else: 4837 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4838 4839 # elif args.buy_order_grid is not None: 4840 # # update order grid work with api v2 4841 # if len(args.buy_order_grid) == 2: 4842 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4843 # 4844 # for order in orderParams: 4845 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4846 # 4847 # else: 4848 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4849 # 4850 # elif args.sell_order_grid is not None: 4851 # # update order grid work with api v2 4852 # if len(args.sell_order_grid) >= 2: 4853 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4854 # 4855 # for order in orderParams: 4856 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4857 # 4858 # else: 4859 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4860 4861 elif args.close_order is not None: 4862 trader.CloseOrders(args.close_order) # close only one order 4863 4864 elif args.close_orders is not None: 4865 trader.CloseOrders(args.close_orders) # close list of orders 4866 4867 elif args.close_trade: 4868 if not (args.ticker or args.figi): 4869 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4870 raise Exception("Ticker or FIGI required") 4871 4872 if args.ticker: 4873 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4874 4875 else: 4876 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4877 4878 elif args.close_trades is not None: 4879 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4880 4881 elif args.close_all is not None: 4882 trader.CloseAll(*args.close_all) 4883 4884 elif args.limits: 4885 if args.output is not None: 4886 trader.withdrawalLimitsFile = args.output 4887 4888 trader.OverviewLimits(show=True) 4889 4890 elif args.user_info: 4891 if args.output is not None: 4892 trader.userInfoFile = args.output 4893 4894 trader.OverviewUserInfo(show=True) 4895 4896 elif args.account: 4897 if args.output is not None: 4898 trader.userAccountsFile = args.output 4899 4900 trader.OverviewAccounts(show=True) 4901 4902 else: 4903 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4904 raise Exception("There is no command to execute") 4905 4906 except Exception: 4907 trace = tb.format_exc() 4908 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4909 if e in trace: 4910 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4911 break 4912 4913 uLogger.debug(trace) 4914 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4915 exitCode = 255 # an error occurred, must be open a ticket for this issue 4916 4917 finally: 4918 finish = datetime.now(tzutc()) 4919 4920 if exitCode == 0: 4921 if args.more: 4922 uLogger.debug("All operations were finished success (summary code is 0).") 4923 4924 else: 4925 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4926 os.path.abspath(uLog.defaultLogFile), exitCode, 4927 )) 4928 4929 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4930 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4931 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4932 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4933 )) 4934 uLogger.debug("=-" * 50) 4935 4936 if not kwargs: 4937 sys.exit(exitCode) 4938 4939 else: 4940 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: